[{"data":1,"prerenderedAt":817},["ShallowReactive",2],{"/de-de/blog/efficient-devsecops-workflows-hands-on-python-gitlab-api-automation":3,"navigation-de-de":43,"banner-de-de":456,"footer-de-de":466,"blog-post-authors-de-de-Michael Friedrich":700,"blog-related-posts-de-de-efficient-devsecops-workflows-hands-on-python-gitlab-api-automation":714,"blog-promotions-de-de":754,"next-steps-de-de":807},{"id":4,"title":5,"authorSlugs":6,"authors":8,"body":10,"category":11,"categorySlug":11,"config":12,"content":16,"date":20,"description":17,"extension":27,"externalUrl":28,"featured":14,"heroImage":19,"isFeatured":14,"meta":29,"navigation":30,"path":31,"publishedDate":20,"rawbody":32,"seo":33,"slug":13,"stem":37,"tagSlugs":38,"tags":41,"template":15,"updatedDate":26,"__hash__":42},"blogPosts/de-de/blog/efficient-devsecops-workflows-hands-on-python-gitlab-api-automation.yml","Effiziente DevSecOps-Workflows: Praktische python-gitlab-API-Automatisierung",[7],"michael-friedrich",[9],"Michael Friedrich","Ein oft zitiertes Sprichwort aus einer Konferenzpräsentation lautet: „Manuelle Arbeit ist ein Fehler“. Bei sich wiederholenden Aufgaben in Arbeitsabläufen ist es ratsam, so viel wie möglich zu automatisieren. Ein Beispiel dafür ist die Nutzung einer REST-API zur Bestandsaufnahme von Einstellungen oder das Erstellen neuer Kommentare in GitLab-Issues und Merge-Anfragen mittels API-Aktionen.\n\n## GitLab-API-Interaktion: Möglichkeiten und Unterstützung durch API-Abstraktionsbibliotheken\n\nDie Interaktion mit der REST-API von GitLab kann auf verschiedene Weise erfolgen: entweder durch HTTP-Anfragen mit curl (oder hurl) in der Befehlszeile oder durch das Schreiben von Skripten in einer Programmiersprache. Letzteres kann dazu führen, dass der Code für HTTP-Anfragen und das Parsen der JSON-Antworten neu entwickelt werden muss. Glücklicherweise unterstützt die umfangreiche GitLab-Community viele Programmiersprachen mit API-Abstraktionsbibliotheken.\n\nDiese Bibliotheken bieten Unterstützung für alle API-Attribute, fügen Hilfsfunktionen zum Abrufen, Erstellen und Löschen von Objekten hinzu und helfen Entwicklern dabei, sich auf ihre Kernaufgaben zu konzentrieren. Die [python-gitlab Bibliothek](https://python-gitlab.readthedocs.io/en/stable/) ist ein besonders funktionsreiches und einfach zu verwendendes Beispiel für eine solche Bibliothek in Python.\n\nIn diesem Blogbeitrag wird die grundlegende Nutzung der python-gitlab-Bibliothek erläutert, einschließlich der Arbeit mit API-Objekten, Attributen, Paginierung und Resultsets. Zudem werden spezifischere Anwendungsfälle vorgestellt, in denen Daten gesammelt, Zusammenfassungen gedruckt und Daten in die API geschrieben werden, um Kommentare und Commits zu erstellen. Viele dieser Anwendungsfälle basieren auf Fragen aus der Community in Foren, auf Hacker News oder in Issues.\n\n## Inhaltsverzeichnis\n- [GitLab-API-Interaktion: Möglichkeiten und Unterstützung durch API-Abstraktionsbibliotheken](#gitlab-api-interaktion-möglichkeiten-und-unterstützung-durch-api-abstraktionsbibliotheken)\n- [Los geht's: python-gitlab Install](#los-geht's-python-gitlab-install)\n- [GitLab API Python: Konfiguration](#gitlab-api-python-konfiguration)\n- [Objekte verwalten: das GitLab-Objekt](#objekte-verwalten-das-gitlab-objekt)\n  - [Objektmanager und Laden von Objekten](#objektmanager-und-laden-von-objekten)\n  - [Paginierung der Ergebnisse](#paginierung-der-ergebnisse)\n  - [Arbeiten mit Objektbeziehungen](#arbeiten-mit-objektbeziehungen)\n  - [Arbeiten mit verschiedenen Objektsammlungsbereichen](#arbeiten-mit-verschiedenen-objektsammlungsbereichen)\n- [DevSecOps-Anwendungsfälle für API-Leseaktionen](#devsecops-anwendungsfälle-für-api-leseaktionen)\n  - [Zweige nach Zusammenführungsstatus auflisten](#zweige-nach-zusammenführungsstatus-auflisten)\n  - [Drucken von Projekteinstellungen zur Überprüfung: MR-Genehmigungsregeln](#drucken-von-projekteinstellungen-zur-überprüfung-mr-genehmigungsregeln)\n  - [Inventarisierung: Abrufen aller CI/CD-Variablen, die geschützt oder maskiert sind](#inventarisierung-abrufen-aller-cicd-variablen-die-geschützt-oder-maskiert-sind)\n  - [Herunterladen einer Datei aus dem Repository](#herunterladen-einer-datei-aus-dem-repository)\n  - [Hilfe zur Migration: Auflistung aller zertifikatsbasierten Kubernetes-Cluster](#hilfe-zur-migration-auflistung-aller-zertifikatsbasierten-kubernetes-cluster)\n  - [Team-Effizienz: Prüfe, ob bestehende Merge-Requests nach dem Mergen einer großen Refactoring-MR neu gebasht werden müssen](#team-effizienz-prüfe-ob-bestehende-merge-requests-nach-dem-mergen-einer-großen-refactoring-mr-neu-gebasht-werden-müssen)\n- [DevSecOps-Anwendungsfälle für API-Schreibaktionen](#devsecops-anwendungsfälle-für-api-schreibaktionen)\n  - [Verschieben von Epics zwischen Gruppen](#verschieben-von-epics-zwischen-gruppen)\n  - [Automatisierung des Verschiebens von Epics](#automatisierung-des-verschiebens-von-epics)\n  - [Compliance: Sicherstellen, dass Projekteinstellungen nicht überschrieben werden](#compliance-sicherstellen-dass-projekteinstellungen-nicht-überschrieben-werden)\n  - [Notizen machen, Fälligkeitsübersicht erstellen](#notizen-machen-fälligkeitsübersicht-erstellen)\n\n## Los geht's: python-gitlab Install\n\nDie Dokumentation von python-gitlab bietet eine umfassende Einführung in die Nutzung, einschließlich Anleitungen für den Einstieg, Informationen zu Objekttypen und deren Methoden sowie kombinierte Workflow-Beispiele. Ergänzend dazu ist die Dokumentation der GitLab-API-Ressourcen hilfreich, da sie die verfügbaren Objektattribute detailliert beschreibt. Zusammen sind diese beiden Dokumentationen die besten Ressourcen für den Einstieg.\n\nDie Code-Beispiele in diesem Blogbeitrag setzen Python 3.8+ und die python-gitlab-Bibliothek voraus. Weitere notwendige Abhängigkeiten sind in der Datei requirements.txt aufgeführt. Ein Beispiel erfordert die Bibliothek pyyaml zum Parsen von YAML-Konfigurationen. Um den Code der Anwendungsfälle nachzuvollziehen und zu üben, empfiehlt es sich, das Projekt zu klonen, die Anforderungen zu installieren und die Skripte auszuführen.\n\n```shell\ngit clone https://gitlab.com/gitlab-da/use-cases/gitlab-api/gitlab-api-python.git\n\ncd gitlab-api-python\n\nbrew install python\n\npip3 install -r requirements.txt\n\npython3 \u003Cscriptname>.py\n```\nDie Skripte verwenden absichtlich keine gemeinsam genutzte Bibliothek, die z. B. generische Funktionen für das Lesen von Parametern oder zusätzliche Hilfsfunktionen bereitstellt. Die Idee ist, einfach zu verstehende Beispiele zu zeigen, die eigenständig zum Testen verwendet werden können und lediglich die Installation der python-gitlab-Bibliothek erfordern.\n\nEs wird empfohlen, den Code für den Produktionseinsatz zu verbessern. Dies kann auch beim Aufbau eines gewarteten API-Tooling-Projekts helfen, das z. B. Container-Images und CI/CD-Vorlagen für Entwickler enthält, die auf einer DevSecOps-Plattform genutzt werden können.\n\n## GitLab API Python: Konfiguration\n\nOhne Konfiguration führt python-gitlab unauthentifizierte Anfragen an den Standardserver https://gitlab.com aus. Die häufigsten Konfigurationseinstellungen beziehen sich auf die GitLab-Instanz, mit der eine Verbindung hergestellt werden soll, und die Authentifizierungsmethode durch Angabe von Zugriffstokens. python-gitlab unterstützt verschiedene Arten der Konfiguration: Eine Konfigurationsdatei oder Umgebungsvariablen.\n\nDie Konfigurationsdatei ist für die API-Bibliotheksbindungen und die CLI verfügbar (die CLI wird in diesem Blogpost nicht erläutert). Die Konfigurationsdatei unterstützt Credential Helpers für den direkten Zugriff auf Token.\n\nUmgebungsvariablen als alternative Konfigurationsmethode bieten eine einfache Möglichkeit, das Skript auf dem Terminal auszuführen, in Container-Images zu integrieren und sie für die Ausführung in CI/CD-Pipelines vorzubereiten.\n\nDie Konfiguration muss in den Kontext des Python-Skripts geladen werden. Beginne mit dem Import der os-Bibliothek, um die Umgebungsvariablen mit der Methode os.environ.get() abzurufen. Der erste Parameter gibt den Schlüssel an, der zweite Parameter legt den Standardwert fest, wenn die Variable in der Umgebung nicht verfügbar ist.\n\n```python\nimport os\n\ngl_server = os.environ.get('GL_SERVER', 'https://gitlab.com')\n\nprint(gl_server)\n```\n\nDie Parametrisierung auf dem Terminal kann direkt nur für den Befehl erfolgen oder in die Shell-Umgebung exportiert werden.\n\n```shell\n$ GL_SERVER=’https://gitlab.company.com’ python3 script.py\n\n$ export GL_SERVER=’https://gitlab.company.com’\n$ python3 script.py\n```\n\nEs wird empfohlen, Sicherheitsprüfungen hinzuzufügen, um sicherzustellen, dass alle Variablen gesetzt sind, bevor das Programm weiter ausgeführt wird. Der folgende Ausschnitt importiert die erforderlichen Bibliotheken, liest die Umgebungsvariable GL_SERVER und erwartet, dass der Benutzer die Variable GL_TOKEN setzt. Wenn dies nicht der Fall ist, gibt das Skript Fehler aus und ruft sys.exit(1) auf, um einen Fehlerstatus anzuzeigen.\n\n```python\nimport gitlab\nimport os\nimport sys\n\nGITLAB_SERVER = os.environ.get('GL_SERVER', 'https://gitlab.com')\nGITLAB_TOKEN = os.environ.get('GL_TOKEN')\n\nif not GITLAB_TOKEN:\n    print(\"Please set the GL_TOKEN env variable.\")\n    sys.exit(1)\n\n```\n\nFolgend siehst du ein ausführlicheres Beispiel, das eine Verbindung zur API herstellt und eine tatsächliche Datenabfrage durchführt.\n\n## Objekte verwalten: das GitLab-Objekt\n\nFür jede Interaktion mit der API muss das GitLab-Objekt instanziiert werden. Dies ist der Einstiegspunkt für die Konfiguration des GitLab-Servers für die Verbindung, die Authentifizierung mithilfe von Zugriffstokens und weitere globale Einstellungen für die Paginierung, das Laden von Objekten und mehr.\n\nIm folgenden Beispiel wird eine nicht authentifizierte Anfrage an gitlab.com ausgeführt. Es ist möglich, auf öffentliche API-Endpunkte zuzugreifen und zum Beispiel eine bestimmte [.gitignore Vorlage für Python](https://python-gitlab.readthedocs.io/en/stable/gl_objects/templates.html#gitignore-templates) abzurufen.\n\n[python_gitlab_object_unauthenticated.py](https://gitlab.com/gitlab-da/use-cases/gitlab-api/gitlab-api-python/-/blob/main/python_gitlab_object_unauthenticated.py)\n\n```python\nimport gitlab\n\ngl = gitlab.Gitlab()\n\n# Get .gitignore templates without authentication\ngitignore_templates = gl.gitignores.get('Python')\n\nprint(gitignore_templates.content)\n```\n\nDie nächsten Abschnitte geben weitere Einblicke in:\n\n- Objektmanager und Laden von Objekten\n- Paginierung von Ergebnissen\n- Arbeiten mit Objektbeziehungen\n- Arbeiten mit verschiedenen Objektsammlungsbereichen\n\n### Objektmanager und Laden von Objekten\n\nDie python-gitlab-Bibliothek ermöglicht den Zugriff auf GitLab-Ressourcen über sogenannte „[Manager](https://python-gitlab.readthedocs.io/en/stable/api-usage.html#managers)\". Jeder Managertyp implementiert Methoden zur Arbeit mit den Datensätzen (list, get, etc.).\n\nDas Skript zeigt, wie man auf Untergruppen, direkte Projekte, alle Projekte einschließlich Untergruppen, Issues, Epics und To-dos zugreifen kann. Diese Methoden und der API-Endpunkt erfordern eine Authentifizierung für den Zugriff auf alle Attribute. Der Codeschnipsel verwendet daher Variablen, um das Authentifizierungs-Token abzurufen sowie die GROUP_ID-Variable, um eine Hauptgruppe anzugeben, bei der die Suche beginnen soll.\n\n```python\n#!/usr/bin/env python\n\nimport gitlab\nimport os\nimport sys\n\nGITLAB_SERVER = os.environ.get('GL_SERVER', 'https://gitlab.com')\n# https://gitlab.com/gitlab-da/use-cases/\nGROUP_ID = os.environ.get('GL_GROUP_ID', 16058698)\nGITLAB_TOKEN = os.environ.get('GL_TOKEN')\n\nif not GITLAB_TOKEN:\n    print(\"Please set the GL_TOKEN env variable.\")\n    sys.exit(1)\n\ngl = gitlab.Gitlab(GITLAB_SERVER, private_token=GITLAB_TOKEN)\n\n# Main\nmain_group = gl.groups.get(GROUP_ID)\n\nprint(\"Sub groups\")\nfor sg in main_group.subgroups.list():\n    print(\"Subgroup name: {sg}\".format(sg=sg.name))\n\nprint(\"Projects (direct)\")\nfor p in main_group.projects.list():\n    print(\"Project name: {p}\".format(p=p.name))\n\nprint(\"Projects (including subgroups)\")\nfor p in main_group.projects.list(include_subgroups=True, all=True):\n     print(\"Project name: {p}\".format(p=p.name))\n\nprint(\"Issues\")\nfor i in main_group.issues.list(state='opened'):\n    print(\"Issue title: {t}\".format(t=i.title))\n\nprint(\"Epics\")\nfor e in main_group.issues.list():\n    print(\"Epic title: {t}\".format(t=e.title))\n\nprint(\"Todos\")\nfor t in gl.todos.list(state='pending'):\n    print(\"Todo: {t} url: {u}\".format(t=t.body, u=t.target_url\n\n```\n\nDu kannst das Skript [`python_gitlab_object_manager_methods.py`](https://gitlab.com/gitlab-da/use-cases/gitlab-api/gitlab-api-python/-/blob/main/python_gitlab_object_manager_methods.py) ausführen, indem du die GROUP_ID-Variable auf GitLab.com SaaS für deine eigene zu analysierende Gruppe überschreibst. Die Variable GL_SERVER muss für selbstverwaltete Instanzziele angegeben werden. GL_TOKEN muss das persönliche Zugriffstoken enthalten.\n\n```shell\nexport GL_TOKEN=xxx\n\nexport GL_SERVER=”https://gitlab.company.com”\n\nexport GL_SERVER=”https://gitlab.com”\n\nexport GL_GROUP_ID=1234\n\npython3 python_gitlab_object_manager_methods.py\n```\nIn Zukunft werden die Beispiel-Snippets die Python-Header und das Parsen von Umgebungsvariablen nicht mehr zeigen, um sich auf den Algorithmus und die Funktionalität zu konzentrieren. Alle Skripte sind Open Source unter der MIT-Lizenz und in [diesem Projekt](https://gitlab.com/gitlab-da/use-cases/gitlab-api/gitlab-api-python) verfügbar.\n\n### Paginierung der Ergebnisse\n\nStandardmäßig gibt die GitLab-API nicht alle Ergebnissätze zurück und erfordert, dass die Clients die Paginierung verwenden, um durch alle Ergebnisseiten zu iterieren. Mit der python-gitlab-Bibliothek können Benutzer die Einstellungen global im GitLab-Objekt oder bei jedem list()-Aufruf [festlegen](https://python-gitlab.readthedocs.io/en/stable/api-usage.html#pagination). Standardmäßig würden alle Ergebnissätze API-Anfragen auslösen, was die Skriptausführung verlangsamen kann. Die empfohlene Methode ist die Verwendung von iterator=True, die ein Generatorobjekt zurückgibt, und API-Aufrufe werden beim Zugriff auf das Objekt bei Bedarf ausgelöst.\n\nDas folgende Beispiel sucht nach dem Gruppennamen everyonecancontribute und verwendet eine [Paginierung](https://docs.gitlab.com/api/rest/#pagination) der Schlüsselsätze mit 100 Ergebnissen auf jeder Seite. Der Iterator wird bei gl.groups.list(iterator=True) auf true gesetzt, um bei Bedarf neue Ergebnissätze abzurufen. Wird der gesuchte Gruppenname gefunden, bricht die Schleife ab und gibt eine Zusammenfassung aus, einschließlich der Messung der Dauer der gesamten Suchanfrage.\n\n```python\nSEARCH_GROUP_NAME=\"everyonecancontribute\"\n\n# Use keyset pagination\n# https://python-gitlab.readthedocs.io/en/stable/api-usage.html#pagination\ngl = gitlab.Gitlab(GITLAB_SERVER, private_token=GITLAB_TOKEN,\n    pagination=\"keyset\", order_by=\"id\", per_page=100)\n\n# Iterate over the list, and fire new API calls in case the result set does not match yet\ngroups = gl.groups.list(iterator=True)\n\nfound_page = 0\nstart = timer()\n\nfor group in groups:\n    if SEARCH_GROUP_NAME == group.name:\n        # print(group) # debug\n        found_page = groups.current_page\n        break\n\nend = timer()\n\nduration = f'{end-start:.2f}'\n\nif found_page > 0:\n    print(\"Pagination API example for Python with GitLab{desc} - found group {g} on page {p}, duration {d}s\".format(\n        desc=\", the DevSecOps platform\", g=SEARCH_GROUP_NAME, p=found_page, d=duration))\nelse:\n    print(\"Could not find group name '{g}', duration {d}\".format(g=SEARCH_GROUP_NAME, d=duration))\n\n```\n\nBeim Ausführen von python_gitlab_pagination.py wurde die Gruppe [everyonecancontribute](https://gitlab.com/everyonecancontribute) auf Seite 5 gefunden.\n\n```shell\n$ python3 python_gitlab_pagination.py\nPagination API example for Python with GitLab, the DevSecOps platform - found group everyonecancontribute on page 5, duration 8.51s\n```\n\n### Arbeiten mit Objektbeziehungen\n\nBei der Arbeit mit Objektbeziehungen – z. B. beim Sammeln aller Projekte in einer bestimmten Gruppe – müssen zusätzliche Schritte unternommen werden. Die zurückgegebenen Projektobjekte enthalten standardmäßig nur begrenzte Attribute. Für verwaltbare Objekte ist ein zusätzlicher get()-Aufruf erforderlich, der das vollständige Projektobjekt von der API im Hintergrund anfordert. Dieser On-Demand-Workflow hilft, Wartezeiten und Datenverkehr zu vermeiden, indem er die sofort zurückgegebenen Attribute reduziert.\n\nDas folgende Beispiel veranschaulicht das Problem, indem es eine Schleife durch alle Projekte in einer Gruppe durchläuft und versucht, die Funktion project.branches.list() aufzurufen, was eine Ausnahme im try/except-Flow auslöst. Im zweiten Beispiel wird ein verwaltbares Projektobjekt ermittelt und der Funktionsaufruf erneut versucht.\n\n```python\n# Main\ngroup = gl.groups.get(GROUP_ID)\n\n# Collect all projects in group and subgroups\nprojects = group.projects.list(include_subgroups=True, all=True)\n\nfor project in projects:\n    # Try running a method on a weak object\n    try:\n       print(\"🤔 Project: {pn} 💡 Branches: {b}\\n\".format(\n        pn=project.name,\n        b=\", \".join([x.name for x in project.branches.list()])))\n    except Exception as e:\n        print(\"Got exception: {e} \\n ===================================== \\n\".format(e=e))\n\n    # Retrieve a full manageable project object\n    # https://python-gitlab.readthedocs.io/en/stable/gl_objects/groups.html#examples\n    manageable_project = gl.projects.get(project.id)\n\n    # Print a method available on a manageable object\n    print(\"🤔 Project: {pn} 💡 Branches: {b}\\n\".format(\n        pn=manageable_project.name,\n        b=\", \".join([x.name for x in manageable_project.branches.list()])))\n\n```\n\nDer Exception-Handler in der python-gitlab-Bibliothek gibt die Fehlermeldung aus und verlinkt auch auf die Dokumentation. Es ist hilfreich, bei der Fehlersuche zu beachten, dass Objekte möglicherweise nicht verwaltet werden können, wenn du nicht auf Objektattribute oder Funktionsaufrufe zugreifen kannst.\n\n```shell\n$ python3 python_gitlab_manageable_objects.py\n\n🤔 Project: GitLab API Playground 💡 Branches: cicd-demo-automated-comments, docs-mr-approval-settings, main\n\nGot exception: 'GroupProject' object has no attribute 'branches'\n\n\u003Cclass 'gitlab.v4.objects.projects.GroupProject'> was created via a\nlist() call and only a subset of the data may be present. To ensure\nall data is present get the object using a get(object.id) call. For\nmore details, see:\n\nhttps://python-gitlab.readthedocs.io/en/v3.8.1/faq.html#attribute-error-list\n =====================================\n\n```\n\nHier findest du das vollständige [Skript](https://gitlab.com/gitlab-da/use-cases/gitlab-api/gitlab-api-python/-/blob/main/python_gitlab_manageable_objects.py).\n\n### Arbeiten mit verschiedenen Objektsammlungsbereichen\n\nManchmal muss das Skript alle Projekte aus einer selbstverwalteten Instanz, aus einer Gruppe mit Untergruppen oder aus einem einzelnen Projekt sammeln. Letzteres ist hilfreich, um die erforderlichen Attribute schneller testen zu können, und der Gruppenabruf hilft später beim Testen im großen Maßstab. Das folgende Snippet sammelt alle Projektobjekte in der projects-Liste und fügt Objekte aus verschiedenen eingehenden Konfigurationen hinzu. Du wirst auch wieder das verwaltbare Objektmuster für Projekte in Gruppen sehen.\n\n```python\n\n    # Collect all projects, or prefer projects from a group id, or a project id\n    projects = []\n\n    # Direct project ID\n    if PROJECT_ID:\n        projects.append(gl.projects.get(PROJECT_ID))\n\n    # Groups and projects inside\n    elif GROUP_ID:\n        group = gl.groups.get(GROUP_ID)\n\n        for project in group.projects.list(include_subgroups=True, all=True):\n            # https://python-gitlab.readthedocs.io/en/stable/gl_objects/groups.html#examples\n            manageable_project = gl.projects.get(project.id)\n            projects.append(manageable_project)\n\n    # All projects on the instance (may take a while to process)\n    else:\n        projects = gl.projects.list(get_all=True)\n\n```\n\nDas vollständige Beispiel befindet sich in [diesem Skript](https://gitlab.com/gitlab-da/use-cases/gitlab-api/gitlab-api-python/-/blob/main/get_mr_approval_rules.py) für die Auflistung der Einstellungen der MR-Genehmigungsregeln für bestimmte Projektziele.\n\n## DevSecOps-Anwendungsfälle für API-Leseaktionen\n\nDas authentifizierte Zugriffstoken benötigt den Bereich [`read_api` scope](https://docs.gitlab.com/user/profile/personal_access_tokens/#personal-access-token-scopes).\n\nDie folgenden Anwendungsfälle werden diskutiert:\n\n- Zweige nach Zusammenführungsstatus auflisten\n- Drucken von Projekteinstellungen zur Überprüfung: MR-Genehmigungsregeln\n- Inventarisierung: Abrufen aller CI/CD-Variablen, die geschützt oder maskiert sind\n- Herunterladen einer Datei aus dem Repository\n- Hilfe zur Migration: Auflistung aller zertifikatsbasierten Kubernetes-Cluster\n- Team-Effizienz: Prüfen Sie, ob bestehende Merge-Requests nach dem Mergen einer großen Refactoring-MR neu gebasht werden müssen\n\n### Zweige nach Zusammenführungsstatus auflisten\n\nEin häufiges Anliegen ist es, in einem Projekt ein wenig Git-Housekeeping zu betreiben und zu sehen, wie viele zusammengeführte und nicht zusammengeführte Zweige im Umlauf sind. Eine [Frage](https://forum.gitlab.com/t/python-gitlab-project-branch-list-filter/80257) im GitLab-Community-Forum zum Filtern von Zweiglisten hat mich dazu inspiriert, ein [Skript](https://gitlab.com/gitlab-da/use-cases/gitlab-api/gitlab-api-python/-/blob/main/get_branches_by_state.py) zu schreiben, mit dem sich dieses Ziel erreichen lässt. Die Methode branches.list() gibt alle Zweigobjekte zurück, die in einer temporären Liste für die spätere Verarbeitung in zwei Schleifen gespeichert werden: Sammeln der Namen der zusammengeführten Zweige und der Namen der nicht zusammengeführten Zweige. Das Attribut merged des branch-Objekts ist ein boolescher Wert, der angibt, ob der Zweig zusammengeführt wurde.\n\n```python\nproject = gl.projects.get(PROJECT_ID, lazy=False, pagination=\"keyset\", order_by=\"updated_at\", per_page=100)\n\n# Get all branches\nreal_branches = []\nfor branch in project.branches.list():\n    real_branches.append(branch)\n\nprint(\"All branches\")\nfor rb in real_branches:\n    print(\"Branch: {b}\".format(b=rb.name))\n\n# Get all merged branches\nmerged_branches_names = []\nfor branch in real_branches:\n    if branch.default:\n        continue # ignore the default branch for merge status\n\n    if branch.merged:\n        merged_branches_names.append(branch.name)\n\nprint(\"Branches merged: {b}\".format(b=\", \".join(merged_branches_names)))\n\n# Get un-merged branches\nnot_merged_branches_names = []\nfor branch in real_branches:\n    if branch.default:\n        continue # ignore the default branch for merge status\n\n    if not branch.merged:\n        not_merged_branches_names.append(branch.name)\n\nprint(\"Branches not merged: {b}\".format(b=\", \".join(not_merged_branches_names)))\n```\n\nDer Arbeitsablauf ist absichtlich schrittweise zu lesen. Du kannst die Optimierung des Python-Codes für die bedingte Zweignamensammlung üben.\n\n### Drucken von Projekteinstellungen zur Überprüfung: MR-Genehmigungsregeln\n\nDas folgende [Skript](https://gitlab.com/gitlab-da/use-cases/gitlab-api/gitlab-api-python/-/blob/main/get_mr_approval_rules.py) geht durch alle gesammelten Projektobjekte und prüft, ob Genehmigungsregeln angegeben sind. Wenn die Länge der Liste größer als Null ist, durchläuft es die Liste in einem Loop und druckt die Einstellungen mit einer JSON-Ph pretty-print-Methode aus.\n\n```python\n\n    # Loop over projects and print the settings\n    # https://python-gitlab.readthedocs.io/en/stable/gl_objects/merge_request_approvals.html\n    for project in projects:\n        if len(project.approvalrules.list()) > 0:\n            #print(project) #debug\n            print(\"# Project: {name}, ID: {id}\\n\\n\".format(name=project.name_with_namespace, id=project.id))\n            print(\"[MR Approval settings]({url}/-/settings/merge_requests)\\n\\n\".format(url=project.web_url))\n\n            for ar in project.approvalrules.list():\n                print(\"## Approval rule: {name}, ID: {id}\".format(name=ar.name, id=ar.id))\n                print(\"\\n```json\\n\")\n                print(json.dumps(ar.attributes, indent=2)) # TODO: can be more beautiful, but serves its purpose with pretty print JSON\n                print(\"\\n```\\n\")\n\n```\n\n### Inventarisierung: Abrufen aller CI/CD-Variablen, die geschützt oder maskiert sind\n\n[CI/CD Variablen](https://docs.gitlab.com/ci/variables/) sind hilfreich für die Pipeline-Parametrisierung und können global auf der Instanz, in Gruppen und in Projekten konfiguriert werden. Auch Daten, Passwörter und andere sensible Informationen können dort gespeichert werden. Manchmal kann es notwendig sein, sich einen Überblick über alle CI/CD-Variablen zu verschaffen, die entweder geschützt oder maskiert sind, um ein Gefühl dafür zu bekommen, wie viele Variablen aktualisiert werden müssen, wenn Token zum Beispiel rotieren.\n\nDas folgende [Skript](https://gitlab.com/gitlab-da/use-cases/gitlab-api/gitlab-api-python/-/blob/main/get_all_cicd_variables_masked_or_protected.py) ruft alle Gruppen und Projekte ab und versucht, die CI/CD-Variablen der globalen Instanz (erfordert Admin-Rechte), der Gruppen und Projekte (erfordert Maintainer-/Eigentümer-Rechte) zu sammeln. Es gibt alle CI/CD-Variablen aus, die entweder geschützt oder maskiert sind, und fügt hinzu, dass ein möglicher geheimer Wert gespeichert ist.\n\n```python\n#!/usr/bin/env python\n\nimport gitlab\nimport os\nimport sys\n\n# Helper function to evaluate secrets and print the variables\ndef eval_print_var(var):\n    if var.protected or var.masked:\n        print(\"🛡️🛡️🛡️ Potential secret: Variable '{name}', protected {p}, masked: {m}\".format(name=var.key,p=var.protected,m=var.masked))\n\nGITLAB_SERVER = os.environ.get('GL_SERVER', 'https://gitlab.com')\nGITLAB_TOKEN = os.environ.get('GL_TOKEN') # token requires maintainer+ permissions. Instance variables require admin access.\nPROJECT_ID = os.environ.get('GL_PROJECT_ID') #optional\nGROUP_ID = os.environ.get('GL_GROUP_ID', 8034603) # https://gitlab.com/everyonecancontribute\n\nif not GITLAB_TOKEN:\n    print(\"🤔 Please set the GL_TOKEN env variable.\")\n    sys.exit(1)\n\ngl = gitlab.Gitlab(GITLAB_SERVER, private_token=GITLAB_TOKEN)\n\n# Collect all projects, or prefer projects from a group id, or a project id\nprojects = []\n# Collect all groups, or prefer group from a group id\ngroups = []\n\n# Direct project ID\nif PROJECT_ID:\n    projects.append(gl.projects.get(PROJECT_ID))\n\n# Groups and projects inside\nelif GROUP_ID:\n    group = gl.groups.get(GROUP_ID)\n\n    for project in group.projects.list(include_subgroups=True, all=True):\n        # https://python-gitlab.readthedocs.io/en/stable/gl_objects/groups.html#examples\n        manageable_project = gl.projects.get(project.id)\n        projects.append(manageable_project)\n\n    groups.append(group)\n\n# All projects/groups on the instance (may take a while to process, use iterators to fetch on-demand).\nelse:\n    projects = gl.projects.list(iterator=True)\n    groups = gl.groups.list(iterator=True)\n\nprint(\"# List of all CI/CD variables marked as secret (instance, groups, projects)\")\n\n# https://python-gitlab.readthedocs.io/en/stable/gl_objects/variables.html\n\n# Instance variables (if the token has permissions)\nprint(\"Instance variables, if accessible\")\ntry:\n    for i_var in gl.variables.list(iterator=True):\n        eval_print_var(i_var)\nexcept:\n    print(\"No permission to fetch global instance variables, continueing without.\")\n    print(\"\\n\")\n\n# group variables (maintainer permissions for groups required)\nfor group in groups:\n    print(\"Group {n}, URL: {u}\".format(n=group.full_path, u=group.web_url))\n    for g_var in group.variables.list(iterator=True):\n        eval_print_var(g_var)\n\n    print(\"\\n\")\n\n# Loop over projects and print the settings\nfor project in projects:\n    # skip archived projects, they throw 403 errors\n    if project.archived:\n        continue\n\n    print(\"Project {n}, URL: {u}\".format(n=project.path_with_namespace, u=project.web_url))\n    for p_var in project.variables.list(iterator=True):\n        eval_print_var(p_var)\n\n    print(\"\\n\")\n\n```\nDas Skript druckt die Variablenwerte absichtlich nicht aus; dies soll als Übung für sichere Umgebungen dienen. Für die Speicherung von Daten empfiehlt sich die Verwendung [externer Anbieter](https://docs.gitlab.com/ci/secrets/).\n\n### Herunterladen einer Datei aus dem Repository\n\nZiel des [Skripts](https://gitlab.com/gitlab-da/use-cases/gitlab-api/gitlab-api-python/-/blob/main/get_raw_file_content.py) ist es, einen Dateipfad von einem angegebenen Verzweigungsnamen herunterzuladen und dessen Inhalt in einer neuen Datei zu speichern.\n\n```python\n# Goal: Try to download README.md from https://gitlab.com/gitlab-da/use-cases/gitlab-api/gitlab-api-python/-/blob/main/README.md\nFILE_NAME = 'README.md'\nBRANCH_NAME = 'main'\n\n# Search the file in the repository tree and get the raw blob\nfor f in project.repository_tree():\n    print(\"File path '{name}' with id '{id}'\".format(name=f['name'], id=f['id']))\n\n    if f['name'] == FILE_NAME:\n        f_content = project.repository_raw_blob(f['id'])\n        print(f_content)\n\n# Alternative approach: Get the raw file from the main branch\nraw_content = project.files.raw(file_path=FILE_NAME, ref=BRANCH_NAME)\nprint(raw_content)\n\n# Store the file on disk\nwith open('raw_README.md', 'wb') as f:\n    project.files.raw(file_path=FILE_NAME, ref=BRANCH_NAME, streamed=True, action=f.write)\n\n```\n\n### Hilfe zur Migration: Auflistung aller zertifikatsbasierten Kubernetes-Cluster\n\nDie zertifikatsbasierte Integration von Kubernetes-Clustern in GitLab wurde [abgeschafft](https://docs.gitlab.com/update/deprecations/#self-managed-certificate-based-integration-with-kubernetes). Um Migrationspläne zu unterstützen, kann die Erfassung bestehender Gruppen und Projekte mithilfe der GitLab-API automatisiert werden.\n\n```python\ngroups = [ ]\n\n# get GROUP_ID group\ngroups.append(gl.groups.get(GROUP_ID))\n\nfor group in groups:\n    for sg in group.subgroups.list(include_subgroups=True, all=True):\n        real_group = gl.groups.get(sg.id)\n        groups.append(real_group)\n\ngroup_clusters = {}\nproject_clusters = {}\n\nfor group in groups:\n    #Collect group clusters\n    g_clusters = group.clusters.list()\n\n    if len(g_clusters) > 0:\n        group_clusters[group.id] = g_clusters\n\n    # Collect all projects in group and subgroups and their clusters\n    projects = group.projects.list(include_subgroups=True, all=True)\n\n    for project in projects:\n        # https://python-gitlab.readthedocs.io/en/stable/gl_objects/groups.html#examples\n        manageable_project = gl.projects.get(project.id)\n\n        # skip archived projects\n        if project.archived:\n            continue\n\n        p_clusters = manageable_project.clusters.list()\n\n        if len(p_clusters) > 0:\n            project_clusters[project.id] = p_clusters\n\n# Print summary\nprint(\"## Group clusters\\n\\n\")\nfor g_id, g_clusters in group_clusters.items():\n    url = gl.groups.get(g_id).web_url\n    print(\"Group ID {g_id}: {u}\\n\\n\".format(g_id=g_id, u=url))\n    print_clusters(g_clusters)\n\nprint(\"## Project clusters\\n\\n\")\nfor p_id, p_clusters in project_clusters.items():\n    url = gl.projects.get(p_id).web_url\n    print(\"Project ID {p_id}: {u}\\n\\n\".format(p_id=p_id, u=url))\n    print_clusters(p_clusters)\n\n```\n\nHier findest du das vollständige [Skript](https://gitlab.com/gitlab-da/use-cases/gitlab-api/gitlab-api-python/-/blob/main/list_cert_based_kubernetes_clusters.py).\n\n### Team-Effizienz: Prüfe, ob bestehende Merge-Requests nach dem Mergen einer großen Refactoring-MR neu gebasht werden müssen\n\nDas [GitLab-Handbuch-Repository](https://handbook.gitlab.com/handbook/) ist ein großes Monorepo mit zahlreichen Merge-Requests, die erstellt, geprüft, genehmigt und zusammengeführt werden müssen. Einige Prüfungen dauern länger als andere, insbesondere wenn Zusammenführungsanfragen mehrere Seiten betreffen, beispielsweise wenn ein String umbenannt wird oder Änderungen auf [alle](https://handbook.gitlab.com/handbook/about/#count-handbook-pages) Handbuchseiten ausgedehnt werden. Das Marketing-Handbuch wurde umstrukturiert, was zu vielen Verschiebungen oder Umbenennungen von Verzeichnissen und Pfaden führte.\n\nMit der Zeit nahmen die Issues zu, und es bestand die Sorge, dass andere Merge-Anfragen nach dem Zusammenführen der großen Änderungen auf Konflikte stoßen könnten. Es wurde festgestellt, dass Python-Gitlab in der Lage ist, alle Merge-Requests in einem bestimmten Projekt abzurufen, einschließlich Details über den Git-Zweig, geänderte Quellpfade und vieles mehr.\n\nDas daraus resultierende Skript konfiguriert eine Liste von Quellpfaden, die von allen pythongitlab-Merge-Requests berührt werden, und vergleicht die Diffs der Merge-Requests mit mr.diffs.list(), um festzustellen, ob ein Muster mit dem Wert in old_path. übereinstimmt. Bei einer Übereinstimmung protokolliert das Skript diese und speichert die Zusammenführungsanforderung im seen_mr-Wörterbuch für die spätere Zusammenfassung. Zusätzlich werden Attribute gesammelt, um eine Markdown-Aufgabenliste mit URLs zum leichteren Einfügen in Issue-Beschreibungen zu erstellen.\n\n```python\nPATH_PATTERNS = [\n    'path/to/handbook/source/page.md',\n]\n\n# Only list opened MRs\n# https://python-gitlab.readthedocs.io/en/stable/gl_objects/merge_requests.html#project-merge-requests\nmrs = project.mergerequests.list(state='opened', iterator=True)\n\nseen_mr = {}\n\nfor mr in mrs:\n    # https://docs.gitlab.com/api/merge_requests/#list-merge-request-diffs\n    real_mr = project.mergerequests.get(mr.get_id())\n    real_mr_id = real_mr.attributes['iid']\n    real_mr_url = real_mr.attributes['web_url']\n\n    for diff in real_mr.diffs.list(iterator=True):\n        real_diff = real_mr.diffs.get(diff.id)\n\n        for d in real_diff.attributes['diffs']:\n            for p in PATH_PATTERNS:\n                if p in d['old_path']:\n                    print(\"MATCH: {p} in MR {mr_id}, status '{s}', title '{t}' - URL: {mr_url}\".format(\n                        p=p,\n                        mr_id=real_mr_id,\n                        s=mr_status,\n                        t=real_mr.attributes['title'],\n                        mr_url=real_mr_url))\n\n                    if not real_mr_id in seen_mr:\n                        seen_mr[real_mr_id] = real_mr\n\nprint(\"\\n# MRs to update\\n\")\n\nfor id, real_mr in seen_mr.items():\n    print(\"- [ ] !{mr_id} - {mr_url}+ Status: {s}, Title: {t}\".format(\n        mr_id=id,\n        mr_url=real_mr.attributes['web_url'],\n        s=real_mr.attributes['detailed_merge_status'],\n        t=real_mr.attributes['title']))\n\n```\n\n## DevSecOps-Anwendungsfälle für API-Schreibaktionen\n\nDas authentifizierte Zugriffstoken benötigt den vollen Anwendungsbereich der [`api`](https://docs.gitlab.com/user/profile/personal_access_tokens/#personal-access-token-scopes).\n\nDie folgenden Anwendungsfälle werden diskutiert:\n\n- Verschieben von Epics zwischen Gruppen\n- Compliance: Sicherstellen, dass Projekteinstellungen nicht überschrieben werden\n- Notizen machen, Fälligkeitsübersicht erstellen\n\n### Verschieben von Epics zwischen Gruppen\n\nManchmal ist es erforderlich, Epics, ähnlich wie Issues, in eine andere Gruppe zu verschieben. Eine Frage im GitLab-Marketing-Slack-Kanal hat dazu geführt, einen [Funktionsvorschlag](https://gitlab.com/gitlab-org/gitlab/-/issues/12689) für die Benutzeroberfläche und die [Schnellaktionen](/blog/improve-your-gitlab-productivity-with-these-10-tips/) zu prüfen und später über das Schreiben eines API-Skripts nachzudenken, um die Schritte zu automatisieren.\n\n### Automatisierung des Verschiebens von Epics\n\nDie Idee ist einfach: Ein Epic wird von einer Quellgruppe in eine Zielgruppe verschoben, wobei Titel, Beschreibung und Labels kopiert werden. Da Epics es erlauben, Themen zu gruppieren, müssen sie auch dem Ziel-Epic neu zugewiesen werden. Parent-Child-Epic-Relationships müssen dabei berücksichtigt werden: Alle Child-Epics der Quell-Epics müssen dem Ziel-Epic neu zugewiesen werden.\n\nDas folgende Skript sucht zunächst alle [Attribute](https://python-gitlab.readthedocs.io/en/stable/gl_objects/epics.html) des Quellepos und erstellt dann ein neues Zielepos mit den minimalen Attributen: Titel und Beschreibung. Die Liste der Bezeichnungen wird kopiert und die Änderungen werden mit dem save()-Aufruf beibehalten. Die Ausgaben, die dem Epos zugeordnet sind, müssen im Zielepos neu erstellt werden.\n\nDer create()-Aufruf erzeugt das Beziehungselement und nicht ein neues Issue-Objekt selbst. Das Verschieben von Child-Epics erfordert einen anderen Ansatz, da die Beziehung umgekehrt ist: Die parent_id des Child-Epics muss mit der ID des Quell-Epics verglichen und bei Übereinstimmung auf die ID des Ziel-Epics aktualisiert werden. Nachdem alles erfolgreich kopiert wurde, muss das Quell-Epos in den closed-Zustand versetzt werden.\n\n```python\n#!/usr/bin/env python\n\n# Description: Show how epics can be moved between groups, including title, description, labels, child epics and issues.\n# Requirements: python-gitlab Python libraries. GitLab API write access, and maintainer access to all configured groups/projects.\n# Author: Michael Friedrich \u003Cmfriedrich@gitlab.com>\n# License: MIT, (c) 2023-present GitLab B.V.\n\nimport gitlab\nimport os\nimport sys\n\nGITLAB_SERVER = os.environ.get('GL_SERVER', 'https://gitlab.com')\n# https://gitlab.com/gitlab-da/use-cases/gitlab-api\nSOURCE_GROUP_ID = os.environ.get('GL_SOURCE_GROUP_ID', 62378643)\n# https://gitlab.com/gitlab-da/use-cases/gitlab-api/epic-move-target\nTARGET_GROUP_ID = os.environ.get('GL_TARGET_GROUP_ID', 62742177)\n# https://gitlab.com/groups/gitlab-da/use-cases/gitlab-api/-/epics/1\nEPIC_ID = os.environ.get('GL_EPIC_ID', 1)\nGITLAB_TOKEN = os.environ.get('GL_TOKEN')\n\nif not GITLAB_TOKEN:\n    print(\"Please set the GL_TOKEN env variable.\")\n    sys.exit(1)\n\ngl = gitlab.Gitlab(GITLAB_SERVER, private_token=GITLAB_TOKEN)\n\n# Main\n# Goal: Move epic to target group, including title, body, labels, and child epics and issues.\nsource_group = gl.groups.get(SOURCE_GROUP_ID)\ntarget_group = gl.groups.get(TARGET_GROUP_ID)\n\n# Create a new target epic and copy all its items, then close the source epic.\nsource_epic = source_group.epics.get(EPIC_ID)\n# print(source_epic) #debug\n\nepic_title = source_epic.title\nepic_description = source_epic.description\nepic_labels = source_epic.labels\nepic_issues = source_epic.issues.list()\n\n# Create the epic with minimal attributes\ntarget_epic = target_group.epics.create({\n    'title': epic_title,\n    'description': epic_description,\n})\n\n# Assign the list\ntarget_epic.labels = epic_labels\n\n# Persist the changes in the new epic\ntarget_epic.save()\n\n# Epic issues need to be re-assigned in a loop\nfor epic_issue in epic_issues:\n    ei = target_epic.issues.create({'issue_id': epic_issue.id})\n\n# Child epics need to update their parent_id to the new epic\n# Need to search in all epics, use lazy object loading\nfor sge in source_group.epics.list(lazy=True):\n    # this epic has the source epic as parent epic?\n    if sge.parent_id == source_epic.id:\n        # Update the parent id\n        sge.parent_id = target_epic.id\n        sge.save()\n\nprint(\"Copied source epic {source_id} ({source_url}) to target epic {target_id} ({target_url})\".format(\n    source_id=source_epic.id, source_url=source_epic.web_url,\n    target_id=target_epic.id, target_url=target_epic.web_url))\n\n# Close the old epic\nsource_epic.state_event = 'close'\nsource_epic.save()\nprint(\"Closed source epic {source_id} ({source_url})\".format(\n    source_id=source_epic.id, source_url=source_epic.web_url))\n\n```\n\n```shell\n$  python3 move_epic_between_groups.py\nCopied source epic 725341 (https://gitlab.com/groups/gitlab-da/use-cases/gitlab-api/-/epics/1) to target epic 725358 (https://gitlab.com/groups/gitlab-da/use-cases/gitlab-api/epic-move-target/-/epics/6)\nClosed source epic 725341 (https://gitlab.com/groups/gitlab-da/use-cases/gitlab-api/-/epics/1)\n```\n\nDas [Ziel-Epic](https://gitlab.com/groups/gitlab-da/use-cases/gitlab-api/epic-move-target/-/epics/5) wurde erstellt und zeigt das erwartete Ergebnis: Derselbe Titel, dieselbe Beschreibung, dieselben Bezeichnungen, dasselbe untergeordnete Epic und dieselben Issues.\n\n![Target epic which has all attributes copied from the source epic: title, description, labels, child epics, issues](https://about.gitlab.com/images/blogimages/efficient-devsecops-workflows-python-gitlab-handson/python_gitlab_moved_epic_with_all_attributes.png){: .shadow}\n\n**Übung:** Das Skript kopiert noch keine [Kommentare](https://python-gitlab.readthedocs.io/en/stable/gl_objects/notes.html) und [Diskussionsstränge](https://python-gitlab.readthedocs.io/en/stable/gl_objects/discussions.html). Recherchiere und hilf mit, das Skript zu aktualisieren – Merge-Requests willkommen!\n\n### Compliance: Sicherstellen, dass Projekteinstellungen nicht überschrieben werden\n\nProjekt- und Gruppeneinstellungen können versehentlich von Teammitgliedern mit Administratorrechten geändert werden. Die Compliance-Anforderungen müssen erfüllt werden. Ein weiterer Anwendungsfall ist die Verwaltung der Konfiguration mit Infrastructure-as-Code-Tools, um sicherzustellen, dass die Konfiguration von GitLab-Instanzen, -Gruppen, -Projekten usw. erhalten bleibt und immer dieselbe ist. Tools wie Ansible oder Terraform können ein API-Skript aufrufen oder die Python-GitLab-Bibliothek verwenden, um Aufgaben zur Verwaltung von Einstellungen auszuführen.\n\nIm folgenden Beispiel ist nur der main-Zweig geschützt.\n\n![GitLab project settings for repositories and protected branches, main branch](https://about.gitlab.com/images/blogimages/efficient-devsecops-workflows-python-gitlab-handson/python_gitlab_protected_branches_settings_main.png){: .shadow}\n\nNehmen wir an, dass ein neuer production-Zweig hinzugefügt wurde und ebenfalls geschützt werden soll. Das folgende [Skript](https://gitlab.com/gitlab-da/use-cases/gitlab-api/gitlab-api-python/-/blob/main/enforce_protected_branches.py) definiert das Wörterbuch der geschützten Zweige und ihre Zugriffsebenen für Push-/Merge-Berechtigungen auf Maintainer-Ebene und baut die Vergleichslogik auf der Grundlage der [python-gitlab-Dokumentation](https://python-gitlab.readthedocs.io/en/stable/gl_objects/protected_branches.html) zu geschützten Zweigen auf.\n\n```python\n#!/usr/bin/env python\n\nimport gitlab\nimport os\nimport sys\n\nGITLAB_SERVER = os.environ.get('GL_SERVER', 'https://gitlab.com')\n# https://gitlab.com/gitlab-da/use-cases/\nGROUP_ID = os.environ.get('GL_GROUP_ID', 16058698)\nGITLAB_TOKEN = os.environ.get('GL_TOKEN')\n\nPROTECTED_BRANCHES = {\n    'main': {\n        'merge_access_level': gitlab.const.AccessLevel.MAINTAINER,\n        'push_access_level': gitlab.const.AccessLevel.MAINTAINER\n    },\n    'production': {\n        'merge_access_level': gitlab.const.AccessLevel.MAINTAINER,\n        'push_access_level': gitlab.const.AccessLevel.MAINTAINER\n    },\n}\n\nif not GITLAB_TOKEN:\n    print(\"Please set the GL_TOKEN env variable.\")\n    sys.exit(1)\n\ngl = gitlab.Gitlab(GITLAB_SERVER, private_token=GITLAB_TOKEN)\n\n# Main\ngroup = gl.groups.get(GROUP_ID)\n\n# Collect all projects in group and subgroups\nprojects = group.projects.list(include_subgroups=True, all=True)\n\nfor project in projects:\n    # Retrieve a full manageable project object\n    # https://python-gitlab.readthedocs.io/en/stable/gl_objects/groups.html#examples\n    manageable_project = gl.projects.get(project.id)\n\n    # https://python-gitlab.readthedocs.io/en/stable/gl_objects/protected_branches.html\n    protected_branch_names = []\n\n    for pb in manageable_project.protectedbranches.list():\n        manageable_protected_branch = manageable_project.protectedbranches.get(pb.name)\n        print(\"Protected branch name: {n}, merge_access_level: {mal}, push_access_level: {pal}\".format(\n            n=manageable_protected_branch.name,\n            mal=manageable_protected_branch.merge_access_levels,\n            pal=manageable_protected_branch.push_access_levels\n        ))\n\n        protected_branch_names.append(manageable_protected_branch.name)\n\n    for branch_to_protect, levels in PROTECTED_BRANCHES.items():\n        # Fix missing protected branches\n        if branch_to_protect not in protected_branch_names:\n            print(\"Adding branch {n} to protected branches settings\".format(n=branch_to_protect))\n            p_branch = manageable_project.protectedbranches.create({\n                'name': branch_to_protect,\n                'merge_access_level': gitlab.const.AccessLevel.MAINTAINER,\n                'push_access_level': gitlab.const.AccessLevel.MAINTAINER\n            })\n\n```\n\nWenn das Skript ausgeführt wird, werden der bestehende main-Zweig und ein Hinweis, dass die production aktualisiert wird, ausgegeben. Der Screenshot aus den Repository-Einstellungen verdeutlicht diese Aktion.\n\n```shell\n$ python3 enforce_protected_branches.py ─╯\nProtected branch name: main, merge_access_level: [{'id': 67294702, 'access_level': 40, 'access_level_description': 'Maintainers', 'user_id': None, 'group_id': None}], push_access_level: [{'id': 68546039, 'access_level': 40, 'access_level_description': 'Maintainers', 'user_id': None, 'group_id': None}]\nAdding branch production to protected branches settings\n```\n\n![GitLab project settings for repositories and protected branches, main and production branch](https://about.gitlab.com/images/blogimages/efficient-devsecops-workflows-python-gitlab-handson/python_gitlab_protected_branches_settings_main_production.png){: .shadow}\n\n### Notizen machen, Fälligkeitsübersicht erstellen\n\nEine Diskussion auf [Hacker News](https://news.ycombinator.com/item?id=32155848) über Tools zum Erstellen von Notizen hat mich dazu inspiriert, eine Übersicht in Form einer Markdown-Tabelle zu erstellen, die aus Dateien, die Notizen aufnehmen, geholt und nach dem geparsten Fälligkeitsdatum sortiert wird. Das [Skript](https://gitlab.com/gitlab-da/use-cases/gitlab-api/gitlab-api-python/-/blob/main/generate_snippets_index_by_due_date.py) ist hier zu finden und etwas komplexer zu verstehen.\n\n\u003C!--\n# 2022-07-19 Notes\n\nHN topic about taking notes: https://news.ycombinator.com/item?id=32152935\n\n-->\n\n***Die englischsprachige [Originalversion](https://about.gitlab.com/blog/efficient-devsecops-workflows-hands-on-python-gitlab-api-automation/) dieses Artikels wurde bereits aktualisiert und erhält einige weitere Tipps, welche wir der deutschen Version beizeiten hinzufügen werden.***\n","engineering",{"slug":13,"featured":14,"template":15},"efficient-devsecops-workflows-hands-on-python-gitlab-api-automation",false,"BlogPost",{"title":5,"description":17,"authors":18,"heroImage":19,"date":20,"body":10,"category":11,"tags":21,"updatedDate":26},"Die Python-GitLab-Bibliothek ist eine nützliche Basis für die GitLab-API. In diesem Tutorial erfährst du mehr über praktische Beispiele und bewährte Verfahren.",[9],"https://res.cloudinary.com/about-gitlab-com/image/upload/v1749659883/Blog/Hero%20Images/post-cover-image.jpg","2023-02-01",[22,23,24,25],"integrations","tutorial","DevSecOps","DevSecOps platform","2025-05-16","yml",null,{},true,"/de-de/blog/efficient-devsecops-workflows-hands-on-python-gitlab-api-automation","seo:\n  title: 'Effiziente DevSecOps-Workflows: Praktische\n    python-gitlab-API-Automatisierung'\n  description: >-\n    Die Python-GitLab-Bibliothek ist eine nützliche Basis für die GitLab-API. In\n    diesem Tutorial erfährst du mehr über praktische Beispiele und bewährte\n    Verfahren.\n  ogTitle: 'Effiziente DevSecOps-Workflows: Praktische\n    python-gitlab-API-Automatisierung'\n  ogDescription: >-\n    Die Python-GitLab-Bibliothek ist eine nützliche Basis für die GitLab-API. In\n    diesem Tutorial erfährst du mehr über praktische Beispiele und bewährte\n    Verfahren.\n  noIndex: false\n  ogImage: >-\n    https://res.cloudinary.com/about-gitlab-com/image/upload/v1749659883/Blog/Hero%20Images/post-cover-image.jpg\n  ogUrl: >-\n    https://about.gitlab.com/blog/efficient-devsecops-workflows-hands-on-python-gitlab-api-automation\n  ogSiteName: https://about.gitlab.com\n  ogType: article\n  canonicalUrls: >-\n    https://about.gitlab.com/blog/efficient-devsecops-workflows-hands-on-python-gitlab-api-automation\ncontent:\n  title: 'Effiziente DevSecOps-Workflows: Praktische\n    python-gitlab-API-Automatisierung'\n  description: >-\n    Die Python-GitLab-Bibliothek ist eine nützliche Basis für die GitLab-API. In\n    diesem Tutorial erfährst du mehr über praktische Beispiele und bewährte\n    Verfahren.\n  authors:\n    - Michael Friedrich\n  heroImage: >-\n    https://res.cloudinary.com/about-gitlab-com/image/upload/v1749659883/Blog/Hero%20Images/post-cover-image.jpg\n  date: '2023-02-01'\n  body: >\n    Ein oft zitiertes Sprichwort aus einer Konferenzpräsentation lautet:\n    „Manuelle Arbeit ist ein Fehler“. Bei sich wiederholenden Aufgaben in\n    Arbeitsabläufen ist es ratsam, so viel wie möglich zu automatisieren. Ein\n    Beispiel dafür ist die Nutzung einer REST-API zur Bestandsaufnahme von\n    Einstellungen oder das Erstellen neuer Kommentare in GitLab-Issues und\n    Merge-Anfragen mittels API-Aktionen.\n\n\n    ## GitLab-API-Interaktion: Möglichkeiten und Unterstützung durch\n    API-Abstraktionsbibliotheken\n\n\n    Die Interaktion mit der REST-API von GitLab kann auf verschiedene Weise\n    erfolgen: entweder durch HTTP-Anfragen mit curl (oder hurl) in der\n    Befehlszeile oder durch das Schreiben von Skripten in einer\n    Programmiersprache. Letzteres kann dazu führen, dass der Code für\n    HTTP-Anfragen und das Parsen der JSON-Antworten neu entwickelt werden muss.\n    Glücklicherweise unterstützt die umfangreiche GitLab-Community viele\n    Programmiersprachen mit API-Abstraktionsbibliotheken.\n\n\n    Diese Bibliotheken bieten Unterstützung für alle API-Attribute, fügen\n    Hilfsfunktionen zum Abrufen, Erstellen und Löschen von Objekten hinzu und\n    helfen Entwicklern dabei, sich auf ihre Kernaufgaben zu konzentrieren. Die\n    [python-gitlab Bibliothek](https://python-gitlab.readthedocs.io/en/stable/)\n    ist ein besonders funktionsreiches und einfach zu verwendendes Beispiel für\n    eine solche Bibliothek in Python.\n\n\n    In diesem Blogbeitrag wird die grundlegende Nutzung der\n    python-gitlab-Bibliothek erläutert, einschließlich der Arbeit mit\n    API-Objekten, Attributen, Paginierung und Resultsets. Zudem werden\n    spezifischere Anwendungsfälle vorgestellt, in denen Daten gesammelt,\n    Zusammenfassungen gedruckt und Daten in die API geschrieben werden, um\n    Kommentare und Commits zu erstellen. Viele dieser Anwendungsfälle basieren\n    auf Fragen aus der Community in Foren, auf Hacker News oder in Issues.\n\n\n    ## Inhaltsverzeichnis\n\n    - [GitLab-API-Interaktion: Möglichkeiten und Unterstützung durch\n    API-Abstraktionsbibliotheken](#gitlab-api-interaktion-möglichkeiten-und-unterstützung-durch-api-abstraktionsbibliotheken)\n\n    - [Los geht's: python-gitlab Install](#los-geht's-python-gitlab-install)\n\n    - [GitLab API Python: Konfiguration](#gitlab-api-python-konfiguration)\n\n    - [Objekte verwalten: das\n    GitLab-Objekt](#objekte-verwalten-das-gitlab-objekt)\n      - [Objektmanager und Laden von Objekten](#objektmanager-und-laden-von-objekten)\n      - [Paginierung der Ergebnisse](#paginierung-der-ergebnisse)\n      - [Arbeiten mit Objektbeziehungen](#arbeiten-mit-objektbeziehungen)\n      - [Arbeiten mit verschiedenen Objektsammlungsbereichen](#arbeiten-mit-verschiedenen-objektsammlungsbereichen)\n    - [DevSecOps-Anwendungsfälle für\n    API-Leseaktionen](#devsecops-anwendungsfälle-für-api-leseaktionen)\n      - [Zweige nach Zusammenführungsstatus auflisten](#zweige-nach-zusammenführungsstatus-auflisten)\n      - [Drucken von Projekteinstellungen zur Überprüfung: MR-Genehmigungsregeln](#drucken-von-projekteinstellungen-zur-überprüfung-mr-genehmigungsregeln)\n      - [Inventarisierung: Abrufen aller CI/CD-Variablen, die geschützt oder maskiert sind](#inventarisierung-abrufen-aller-cicd-variablen-die-geschützt-oder-maskiert-sind)\n      - [Herunterladen einer Datei aus dem Repository](#herunterladen-einer-datei-aus-dem-repository)\n      - [Hilfe zur Migration: Auflistung aller zertifikatsbasierten Kubernetes-Cluster](#hilfe-zur-migration-auflistung-aller-zertifikatsbasierten-kubernetes-cluster)\n      - [Team-Effizienz: Prüfe, ob bestehende Merge-Requests nach dem Mergen einer großen Refactoring-MR neu gebasht werden müssen](#team-effizienz-prüfe-ob-bestehende-merge-requests-nach-dem-mergen-einer-großen-refactoring-mr-neu-gebasht-werden-müssen)\n    - [DevSecOps-Anwendungsfälle für\n    API-Schreibaktionen](#devsecops-anwendungsfälle-für-api-schreibaktionen)\n      - [Verschieben von Epics zwischen Gruppen](#verschieben-von-epics-zwischen-gruppen)\n      - [Automatisierung des Verschiebens von Epics](#automatisierung-des-verschiebens-von-epics)\n      - [Compliance: Sicherstellen, dass Projekteinstellungen nicht überschrieben werden](#compliance-sicherstellen-dass-projekteinstellungen-nicht-überschrieben-werden)\n      - [Notizen machen, Fälligkeitsübersicht erstellen](#notizen-machen-fälligkeitsübersicht-erstellen)\n\n    ## Los geht's: python-gitlab Install\n\n\n    Die Dokumentation von python-gitlab bietet eine umfassende Einführung in die\n    Nutzung, einschließlich Anleitungen für den Einstieg, Informationen zu\n    Objekttypen und deren Methoden sowie kombinierte Workflow-Beispiele.\n    Ergänzend dazu ist die Dokumentation der GitLab-API-Ressourcen hilfreich, da\n    sie die verfügbaren Objektattribute detailliert beschreibt. Zusammen sind\n    diese beiden Dokumentationen die besten Ressourcen für den Einstieg.\n\n\n    Die Code-Beispiele in diesem Blogbeitrag setzen Python 3.8+ und die\n    python-gitlab-Bibliothek voraus. Weitere notwendige Abhängigkeiten sind in\n    der Datei requirements.txt aufgeführt. Ein Beispiel erfordert die Bibliothek\n    pyyaml zum Parsen von YAML-Konfigurationen. Um den Code der Anwendungsfälle\n    nachzuvollziehen und zu üben, empfiehlt es sich, das Projekt zu klonen, die\n    Anforderungen zu installieren und die Skripte auszuführen.\n\n\n    ```shell\n\n    git clone\n    https://gitlab.com/gitlab-da/use-cases/gitlab-api/gitlab-api-python.git\n\n\n    cd gitlab-api-python\n\n\n    brew install python\n\n\n    pip3 install -r requirements.txt\n\n\n    python3 \u003Cscriptname>.py\n\n    ```\n\n    Die Skripte verwenden absichtlich keine gemeinsam genutzte Bibliothek, die\n    z. B. generische Funktionen für das Lesen von Parametern oder zusätzliche\n    Hilfsfunktionen bereitstellt. Die Idee ist, einfach zu verstehende Beispiele\n    zu zeigen, die eigenständig zum Testen verwendet werden können und lediglich\n    die Installation der python-gitlab-Bibliothek erfordern.\n\n\n    Es wird empfohlen, den Code für den Produktionseinsatz zu verbessern. Dies\n    kann auch beim Aufbau eines gewarteten API-Tooling-Projekts helfen, das z.\n    B. Container-Images und CI/CD-Vorlagen für Entwickler enthält, die auf einer\n    DevSecOps-Plattform genutzt werden können.\n\n\n    ## GitLab API Python: Konfiguration\n\n\n    Ohne Konfiguration führt python-gitlab unauthentifizierte Anfragen an den\n    Standardserver https://gitlab.com aus. Die häufigsten\n    Konfigurationseinstellungen beziehen sich auf die GitLab-Instanz, mit der\n    eine Verbindung hergestellt werden soll, und die Authentifizierungsmethode\n    durch Angabe von Zugriffstokens. python-gitlab unterstützt verschiedene\n    Arten der Konfiguration: Eine Konfigurationsdatei oder Umgebungsvariablen.\n\n\n    Die Konfigurationsdatei ist für die API-Bibliotheksbindungen und die CLI\n    verfügbar (die CLI wird in diesem Blogpost nicht erläutert). Die\n    Konfigurationsdatei unterstützt Credential Helpers für den direkten Zugriff\n    auf Token.\n\n\n    Umgebungsvariablen als alternative Konfigurationsmethode bieten eine\n    einfache Möglichkeit, das Skript auf dem Terminal auszuführen, in\n    Container-Images zu integrieren und sie für die Ausführung in\n    CI/CD-Pipelines vorzubereiten.\n\n\n    Die Konfiguration muss in den Kontext des Python-Skripts geladen werden.\n    Beginne mit dem Import der os-Bibliothek, um die Umgebungsvariablen mit der\n    Methode os.environ.get() abzurufen. Der erste Parameter gibt den Schlüssel\n    an, der zweite Parameter legt den Standardwert fest, wenn die Variable in\n    der Umgebung nicht verfügbar ist.\n\n\n    ```python\n\n    import os\n\n\n    gl_server = os.environ.get('GL_SERVER', 'https://gitlab.com')\n\n\n    print(gl_server)\n\n    ```\n\n\n    Die Parametrisierung auf dem Terminal kann direkt nur für den Befehl\n    erfolgen oder in die Shell-Umgebung exportiert werden.\n\n\n    ```shell\n\n    $ GL_SERVER=’https://gitlab.company.com’ python3 script.py\n\n\n    $ export GL_SERVER=’https://gitlab.company.com’\n\n    $ python3 script.py\n\n    ```\n\n\n    Es wird empfohlen, Sicherheitsprüfungen hinzuzufügen, um sicherzustellen,\n    dass alle Variablen gesetzt sind, bevor das Programm weiter ausgeführt wird.\n    Der folgende Ausschnitt importiert die erforderlichen Bibliotheken, liest\n    die Umgebungsvariable GL_SERVER und erwartet, dass der Benutzer die Variable\n    GL_TOKEN setzt. Wenn dies nicht der Fall ist, gibt das Skript Fehler aus und\n    ruft sys.exit(1) auf, um einen Fehlerstatus anzuzeigen.\n\n\n    ```python\n\n    import gitlab\n\n    import os\n\n    import sys\n\n\n    GITLAB_SERVER = os.environ.get('GL_SERVER', 'https://gitlab.com')\n\n    GITLAB_TOKEN = os.environ.get('GL_TOKEN')\n\n\n    if not GITLAB_TOKEN:\n        print(\"Please set the GL_TOKEN env variable.\")\n        sys.exit(1)\n\n    ```\n\n\n    Folgend siehst du ein ausführlicheres Beispiel, das eine Verbindung zur API\n    herstellt und eine tatsächliche Datenabfrage durchführt.\n\n\n    ## Objekte verwalten: das GitLab-Objekt\n\n\n    Für jede Interaktion mit der API muss das GitLab-Objekt instanziiert werden.\n    Dies ist der Einstiegspunkt für die Konfiguration des GitLab-Servers für die\n    Verbindung, die Authentifizierung mithilfe von Zugriffstokens und weitere\n    globale Einstellungen für die Paginierung, das Laden von Objekten und mehr.\n\n\n    Im folgenden Beispiel wird eine nicht authentifizierte Anfrage an gitlab.com\n    ausgeführt. Es ist möglich, auf öffentliche API-Endpunkte zuzugreifen und\n    zum Beispiel eine bestimmte [.gitignore Vorlage für\n    Python](https://python-gitlab.readthedocs.io/en/stable/gl_objects/templates.html#gitignore-templates)\n    abzurufen.\n\n\n    [python_gitlab_object_unauthenticated.py](https://gitlab.com/gitlab-da/use-cases/gitlab-api/gitlab-api-python/-/blob/main/python_gitlab_object_unauthenticated.py)\n\n\n    ```python\n\n    import gitlab\n\n\n    gl = gitlab.Gitlab()\n\n\n    # Get .gitignore templates without authentication\n\n    gitignore_templates = gl.gitignores.get('Python')\n\n\n    print(gitignore_templates.content)\n\n    ```\n\n\n    Die nächsten Abschnitte geben weitere Einblicke in:\n\n\n    - Objektmanager und Laden von Objekten\n\n    - Paginierung von Ergebnissen\n\n    - Arbeiten mit Objektbeziehungen\n\n    - Arbeiten mit verschiedenen Objektsammlungsbereichen\n\n\n    ### Objektmanager und Laden von Objekten\n\n\n    Die python-gitlab-Bibliothek ermöglicht den Zugriff auf GitLab-Ressourcen\n    über sogenannte\n    „[Manager](https://python-gitlab.readthedocs.io/en/stable/api-usage.html#managers)\".\n    Jeder Managertyp implementiert Methoden zur Arbeit mit den Datensätzen\n    (list, get, etc.).\n\n\n    Das Skript zeigt, wie man auf Untergruppen, direkte Projekte, alle Projekte\n    einschließlich Untergruppen, Issues, Epics und To-dos zugreifen kann. Diese\n    Methoden und der API-Endpunkt erfordern eine Authentifizierung für den\n    Zugriff auf alle Attribute. Der Codeschnipsel verwendet daher Variablen, um\n    das Authentifizierungs-Token abzurufen sowie die GROUP_ID-Variable, um eine\n    Hauptgruppe anzugeben, bei der die Suche beginnen soll.\n\n\n    ```python\n\n    #!/usr/bin/env python\n\n\n    import gitlab\n\n    import os\n\n    import sys\n\n\n    GITLAB_SERVER = os.environ.get('GL_SERVER', 'https://gitlab.com')\n\n    # https://gitlab.com/gitlab-da/use-cases/\n\n    GROUP_ID = os.environ.get('GL_GROUP_ID', 16058698)\n\n    GITLAB_TOKEN = os.environ.get('GL_TOKEN')\n\n\n    if not GITLAB_TOKEN:\n        print(\"Please set the GL_TOKEN env variable.\")\n        sys.exit(1)\n\n    gl = gitlab.Gitlab(GITLAB_SERVER, private_token=GITLAB_TOKEN)\n\n\n    # Main\n\n    main_group = gl.groups.get(GROUP_ID)\n\n\n    print(\"Sub groups\")\n\n    for sg in main_group.subgroups.list():\n        print(\"Subgroup name: {sg}\".format(sg=sg.name))\n\n    print(\"Projects (direct)\")\n\n    for p in main_group.projects.list():\n        print(\"Project name: {p}\".format(p=p.name))\n\n    print(\"Projects (including subgroups)\")\n\n    for p in main_group.projects.list(include_subgroups=True, all=True):\n         print(\"Project name: {p}\".format(p=p.name))\n\n    print(\"Issues\")\n\n    for i in main_group.issues.list(state='opened'):\n        print(\"Issue title: {t}\".format(t=i.title))\n\n    print(\"Epics\")\n\n    for e in main_group.issues.list():\n        print(\"Epic title: {t}\".format(t=e.title))\n\n    print(\"Todos\")\n\n    for t in gl.todos.list(state='pending'):\n        print(\"Todo: {t} url: {u}\".format(t=t.body, u=t.target_url\n\n    ```\n\n\n    Du kannst das Skript\n    [`python_gitlab_object_manager_methods.py`](https://gitlab.com/gitlab-da/use-cases/gitlab-api/gitlab-api-python/-/blob/main/python_gitlab_object_manager_methods.py)\n    ausführen, indem du die GROUP_ID-Variable auf GitLab.com SaaS für deine\n    eigene zu analysierende Gruppe überschreibst. Die Variable GL_SERVER muss\n    für selbstverwaltete Instanzziele angegeben werden. GL_TOKEN muss das\n    persönliche Zugriffstoken enthalten.\n\n\n    ```shell\n\n    export GL_TOKEN=xxx\n\n\n    export GL_SERVER=”https://gitlab.company.com”\n\n\n    export GL_SERVER=”https://gitlab.com”\n\n\n    export GL_GROUP_ID=1234\n\n\n    python3 python_gitlab_object_manager_methods.py\n\n    ```\n\n    In Zukunft werden die Beispiel-Snippets die Python-Header und das Parsen von\n    Umgebungsvariablen nicht mehr zeigen, um sich auf den Algorithmus und die\n    Funktionalität zu konzentrieren. Alle Skripte sind Open Source unter der\n    MIT-Lizenz und in [diesem\n    Projekt](https://gitlab.com/gitlab-da/use-cases/gitlab-api/gitlab-api-python)\n    verfügbar.\n\n\n    ### Paginierung der Ergebnisse\n\n\n    Standardmäßig gibt die GitLab-API nicht alle Ergebnissätze zurück und\n    erfordert, dass die Clients die Paginierung verwenden, um durch alle\n    Ergebnisseiten zu iterieren. Mit der python-gitlab-Bibliothek können\n    Benutzer die Einstellungen global im GitLab-Objekt oder bei jedem\n    list()-Aufruf\n    [festlegen](https://python-gitlab.readthedocs.io/en/stable/api-usage.html#pagination).\n    Standardmäßig würden alle Ergebnissätze API-Anfragen auslösen, was die\n    Skriptausführung verlangsamen kann. Die empfohlene Methode ist die\n    Verwendung von iterator=True, die ein Generatorobjekt zurückgibt, und\n    API-Aufrufe werden beim Zugriff auf das Objekt bei Bedarf ausgelöst.\n\n\n    Das folgende Beispiel sucht nach dem Gruppennamen everyonecancontribute und\n    verwendet eine\n    [Paginierung](https://docs.gitlab.com/api/rest/#pagination) der\n    Schlüsselsätze mit 100 Ergebnissen auf jeder Seite. Der Iterator wird bei\n    gl.groups.list(iterator=True) auf true gesetzt, um bei Bedarf neue\n    Ergebnissätze abzurufen. Wird der gesuchte Gruppenname gefunden, bricht die\n    Schleife ab und gibt eine Zusammenfassung aus, einschließlich der Messung\n    der Dauer der gesamten Suchanfrage.\n\n\n    ```python\n\n    SEARCH_GROUP_NAME=\"everyonecancontribute\"\n\n\n    # Use keyset pagination\n\n    # https://python-gitlab.readthedocs.io/en/stable/api-usage.html#pagination\n\n    gl = gitlab.Gitlab(GITLAB_SERVER, private_token=GITLAB_TOKEN,\n        pagination=\"keyset\", order_by=\"id\", per_page=100)\n\n    # Iterate over the list, and fire new API calls in case the result set does\n    not match yet\n\n    groups = gl.groups.list(iterator=True)\n\n\n    found_page = 0\n\n    start = timer()\n\n\n    for group in groups:\n        if SEARCH_GROUP_NAME == group.name:\n            # print(group) # debug\n            found_page = groups.current_page\n            break\n\n    end = timer()\n\n\n    duration = f'{end-start:.2f}'\n\n\n    if found_page > 0:\n        print(\"Pagination API example for Python with GitLab{desc} - found group {g} on page {p}, duration {d}s\".format(\n            desc=\", the DevSecOps platform\", g=SEARCH_GROUP_NAME, p=found_page, d=duration))\n    else:\n        print(\"Could not find group name '{g}', duration {d}\".format(g=SEARCH_GROUP_NAME, d=duration))\n\n    ```\n\n\n    Beim Ausführen von python_gitlab_pagination.py wurde die Gruppe\n    [everyonecancontribute](https://gitlab.com/everyonecancontribute) auf Seite\n    5 gefunden.\n\n\n    ```shell\n\n    $ python3 python_gitlab_pagination.py\n\n    Pagination API example for Python with GitLab, the DevSecOps platform -\n    found group everyonecancontribute on page 5, duration 8.51s\n\n    ```\n\n\n    ### Arbeiten mit Objektbeziehungen\n\n\n    Bei der Arbeit mit Objektbeziehungen – z. B. beim Sammeln aller Projekte in\n    einer bestimmten Gruppe – müssen zusätzliche Schritte unternommen werden.\n    Die zurückgegebenen Projektobjekte enthalten standardmäßig nur begrenzte\n    Attribute. Für verwaltbare Objekte ist ein zusätzlicher get()-Aufruf\n    erforderlich, der das vollständige Projektobjekt von der API im Hintergrund\n    anfordert. Dieser On-Demand-Workflow hilft, Wartezeiten und Datenverkehr zu\n    vermeiden, indem er die sofort zurückgegebenen Attribute reduziert.\n\n\n    Das folgende Beispiel veranschaulicht das Problem, indem es eine Schleife\n    durch alle Projekte in einer Gruppe durchläuft und versucht, die Funktion\n    project.branches.list() aufzurufen, was eine Ausnahme im try/except-Flow\n    auslöst. Im zweiten Beispiel wird ein verwaltbares Projektobjekt ermittelt\n    und der Funktionsaufruf erneut versucht.\n\n\n    ```python\n\n    # Main\n\n    group = gl.groups.get(GROUP_ID)\n\n\n    # Collect all projects in group and subgroups\n\n    projects = group.projects.list(include_subgroups=True, all=True)\n\n\n    for project in projects:\n        # Try running a method on a weak object\n        try:\n           print(\"🤔 Project: {pn} 💡 Branches: {b}\\n\".format(\n            pn=project.name,\n            b=\", \".join([x.name for x in project.branches.list()])))\n        except Exception as e:\n            print(\"Got exception: {e} \\n ===================================== \\n\".format(e=e))\n\n        # Retrieve a full manageable project object\n        # https://python-gitlab.readthedocs.io/en/stable/gl_objects/groups.html#examples\n        manageable_project = gl.projects.get(project.id)\n\n        # Print a method available on a manageable object\n        print(\"🤔 Project: {pn} 💡 Branches: {b}\\n\".format(\n            pn=manageable_project.name,\n            b=\", \".join([x.name for x in manageable_project.branches.list()])))\n\n    ```\n\n\n    Der Exception-Handler in der python-gitlab-Bibliothek gibt die Fehlermeldung\n    aus und verlinkt auch auf die Dokumentation. Es ist hilfreich, bei der\n    Fehlersuche zu beachten, dass Objekte möglicherweise nicht verwaltet werden\n    können, wenn du nicht auf Objektattribute oder Funktionsaufrufe zugreifen\n    kannst.\n\n\n    ```shell\n\n    $ python3 python_gitlab_manageable_objects.py\n\n\n    🤔 Project: GitLab API Playground 💡 Branches: cicd-demo-automated-comments,\n    docs-mr-approval-settings, main\n\n\n    Got exception: 'GroupProject' object has no attribute 'branches'\n\n\n    \u003Cclass 'gitlab.v4.objects.projects.GroupProject'> was created via a\n\n    list() call and only a subset of the data may be present. To ensure\n\n    all data is present get the object using a get(object.id) call. For\n\n    more details, see:\n\n\n    https://python-gitlab.readthedocs.io/en/v3.8.1/faq.html#attribute-error-list\n     =====================================\n\n    ```\n\n\n    Hier findest du das vollständige\n    [Skript](https://gitlab.com/gitlab-da/use-cases/gitlab-api/gitlab-api-python/-/blob/main/python_gitlab_manageable_objects.py).\n\n\n    ### Arbeiten mit verschiedenen Objektsammlungsbereichen\n\n\n    Manchmal muss das Skript alle Projekte aus einer selbstverwalteten Instanz,\n    aus einer Gruppe mit Untergruppen oder aus einem einzelnen Projekt sammeln.\n    Letzteres ist hilfreich, um die erforderlichen Attribute schneller testen zu\n    können, und der Gruppenabruf hilft später beim Testen im großen Maßstab. Das\n    folgende Snippet sammelt alle Projektobjekte in der projects-Liste und fügt\n    Objekte aus verschiedenen eingehenden Konfigurationen hinzu. Du wirst auch\n    wieder das verwaltbare Objektmuster für Projekte in Gruppen sehen.\n\n\n    ```python\n\n        # Collect all projects, or prefer projects from a group id, or a project id\n        projects = []\n\n        # Direct project ID\n        if PROJECT_ID:\n            projects.append(gl.projects.get(PROJECT_ID))\n\n        # Groups and projects inside\n        elif GROUP_ID:\n            group = gl.groups.get(GROUP_ID)\n\n            for project in group.projects.list(include_subgroups=True, all=True):\n                # https://python-gitlab.readthedocs.io/en/stable/gl_objects/groups.html#examples\n                manageable_project = gl.projects.get(project.id)\n                projects.append(manageable_project)\n\n        # All projects on the instance (may take a while to process)\n        else:\n            projects = gl.projects.list(get_all=True)\n\n    ```\n\n\n    Das vollständige Beispiel befindet sich in [diesem\n    Skript](https://gitlab.com/gitlab-da/use-cases/gitlab-api/gitlab-api-python/-/blob/main/get_mr_approval_rules.py)\n    für die Auflistung der Einstellungen der MR-Genehmigungsregeln für bestimmte\n    Projektziele.\n\n\n    ## DevSecOps-Anwendungsfälle für API-Leseaktionen\n\n\n    Das authentifizierte Zugriffstoken benötigt den Bereich [`read_api`\n    scope](https://docs.gitlab.com/user/profile/personal_access_tokens/#personal-access-token-scopes).\n\n\n    Die folgenden Anwendungsfälle werden diskutiert:\n\n\n    - Zweige nach Zusammenführungsstatus auflisten\n\n    - Drucken von Projekteinstellungen zur Überprüfung: MR-Genehmigungsregeln\n\n    - Inventarisierung: Abrufen aller CI/CD-Variablen, die geschützt oder\n    maskiert sind\n\n    - Herunterladen einer Datei aus dem Repository\n\n    - Hilfe zur Migration: Auflistung aller zertifikatsbasierten\n    Kubernetes-Cluster\n\n    - Team-Effizienz: Prüfen Sie, ob bestehende Merge-Requests nach dem Mergen\n    einer großen Refactoring-MR neu gebasht werden müssen\n\n\n    ### Zweige nach Zusammenführungsstatus auflisten\n\n\n    Ein häufiges Anliegen ist es, in einem Projekt ein wenig Git-Housekeeping zu\n    betreiben und zu sehen, wie viele zusammengeführte und nicht\n    zusammengeführte Zweige im Umlauf sind. Eine\n    [Frage](https://forum.gitlab.com/t/python-gitlab-project-branch-list-filter/80257)\n    im GitLab-Community-Forum zum Filtern von Zweiglisten hat mich dazu\n    inspiriert, ein\n    [Skript](https://gitlab.com/gitlab-da/use-cases/gitlab-api/gitlab-api-python/-/blob/main/get_branches_by_state.py)\n    zu schreiben, mit dem sich dieses Ziel erreichen lässt. Die Methode\n    branches.list() gibt alle Zweigobjekte zurück, die in einer temporären Liste\n    für die spätere Verarbeitung in zwei Schleifen gespeichert werden: Sammeln\n    der Namen der zusammengeführten Zweige und der Namen der nicht\n    zusammengeführten Zweige. Das Attribut merged des branch-Objekts ist ein\n    boolescher Wert, der angibt, ob der Zweig zusammengeführt wurde.\n\n\n    ```python\n\n    project = gl.projects.get(PROJECT_ID, lazy=False, pagination=\"keyset\",\n    order_by=\"updated_at\", per_page=100)\n\n\n    # Get all branches\n\n    real_branches = []\n\n    for branch in project.branches.list():\n        real_branches.append(branch)\n\n    print(\"All branches\")\n\n    for rb in real_branches:\n        print(\"Branch: {b}\".format(b=rb.name))\n\n    # Get all merged branches\n\n    merged_branches_names = []\n\n    for branch in real_branches:\n        if branch.default:\n            continue # ignore the default branch for merge status\n\n        if branch.merged:\n            merged_branches_names.append(branch.name)\n\n    print(\"Branches merged: {b}\".format(b=\", \".join(merged_branches_names)))\n\n\n    # Get un-merged branches\n\n    not_merged_branches_names = []\n\n    for branch in real_branches:\n        if branch.default:\n            continue # ignore the default branch for merge status\n\n        if not branch.merged:\n            not_merged_branches_names.append(branch.name)\n\n    print(\"Branches not merged: {b}\".format(b=\",\n    \".join(not_merged_branches_names)))\n\n    ```\n\n\n    Der Arbeitsablauf ist absichtlich schrittweise zu lesen. Du kannst die\n    Optimierung des Python-Codes für die bedingte Zweignamensammlung üben.\n\n\n    ### Drucken von Projekteinstellungen zur Überprüfung: MR-Genehmigungsregeln\n\n\n    Das folgende\n    [Skript](https://gitlab.com/gitlab-da/use-cases/gitlab-api/gitlab-api-python/-/blob/main/get_mr_approval_rules.py)\n    geht durch alle gesammelten Projektobjekte und prüft, ob Genehmigungsregeln\n    angegeben sind. Wenn die Länge der Liste größer als Null ist, durchläuft es\n    die Liste in einem Loop und druckt die Einstellungen mit einer JSON-Ph\n    pretty-print-Methode aus.\n\n\n    ```python\n\n        # Loop over projects and print the settings\n        # https://python-gitlab.readthedocs.io/en/stable/gl_objects/merge_request_approvals.html\n        for project in projects:\n            if len(project.approvalrules.list()) > 0:\n                #print(project) #debug\n                print(\"# Project: {name}, ID: {id}\\n\\n\".format(name=project.name_with_namespace, id=project.id))\n                print(\"[MR Approval settings]({url}/-/settings/merge_requests)\\n\\n\".format(url=project.web_url))\n\n                for ar in project.approvalrules.list():\n                    print(\"## Approval rule: {name}, ID: {id}\".format(name=ar.name, id=ar.id))\n                    print(\"\\n```json\\n\")\n                    print(json.dumps(ar.attributes, indent=2)) # TODO: can be more beautiful, but serves its purpose with pretty print JSON\n                    print(\"\\n```\\n\")\n\n    ```\n\n\n    ### Inventarisierung: Abrufen aller CI/CD-Variablen, die geschützt oder\n    maskiert sind\n\n\n    [CI/CD Variablen](https://docs.gitlab.com/ci/variables/) sind hilfreich\n    für die Pipeline-Parametrisierung und können global auf der Instanz, in\n    Gruppen und in Projekten konfiguriert werden. Auch Daten, Passwörter und\n    andere sensible Informationen können dort gespeichert werden. Manchmal kann\n    es notwendig sein, sich einen Überblick über alle CI/CD-Variablen zu\n    verschaffen, die entweder geschützt oder maskiert sind, um ein Gefühl dafür\n    zu bekommen, wie viele Variablen aktualisiert werden müssen, wenn Token zum\n    Beispiel rotieren.\n\n\n    Das folgende\n    [Skript](https://gitlab.com/gitlab-da/use-cases/gitlab-api/gitlab-api-python/-/blob/main/get_all_cicd_variables_masked_or_protected.py)\n    ruft alle Gruppen und Projekte ab und versucht, die CI/CD-Variablen der\n    globalen Instanz (erfordert Admin-Rechte), der Gruppen und Projekte\n    (erfordert Maintainer-/Eigentümer-Rechte) zu sammeln. Es gibt alle\n    CI/CD-Variablen aus, die entweder geschützt oder maskiert sind, und fügt\n    hinzu, dass ein möglicher geheimer Wert gespeichert ist.\n\n\n    ```python\n\n    #!/usr/bin/env python\n\n\n    import gitlab\n\n    import os\n\n    import sys\n\n\n    # Helper function to evaluate secrets and print the variables\n\n    def eval_print_var(var):\n        if var.protected or var.masked:\n            print(\"🛡️🛡️🛡️ Potential secret: Variable '{name}', protected {p}, masked: {m}\".format(name=var.key,p=var.protected,m=var.masked))\n\n    GITLAB_SERVER = os.environ.get('GL_SERVER', 'https://gitlab.com')\n\n    GITLAB_TOKEN = os.environ.get('GL_TOKEN') # token requires maintainer+\n    permissions. Instance variables require admin access.\n\n    PROJECT_ID = os.environ.get('GL_PROJECT_ID') #optional\n\n    GROUP_ID = os.environ.get('GL_GROUP_ID', 8034603) #\n    https://gitlab.com/everyonecancontribute\n\n\n    if not GITLAB_TOKEN:\n        print(\"🤔 Please set the GL_TOKEN env variable.\")\n        sys.exit(1)\n\n    gl = gitlab.Gitlab(GITLAB_SERVER, private_token=GITLAB_TOKEN)\n\n\n    # Collect all projects, or prefer projects from a group id, or a project id\n\n    projects = []\n\n    # Collect all groups, or prefer group from a group id\n\n    groups = []\n\n\n    # Direct project ID\n\n    if PROJECT_ID:\n        projects.append(gl.projects.get(PROJECT_ID))\n\n    # Groups and projects inside\n\n    elif GROUP_ID:\n        group = gl.groups.get(GROUP_ID)\n\n        for project in group.projects.list(include_subgroups=True, all=True):\n            # https://python-gitlab.readthedocs.io/en/stable/gl_objects/groups.html#examples\n            manageable_project = gl.projects.get(project.id)\n            projects.append(manageable_project)\n\n        groups.append(group)\n\n    # All projects/groups on the instance (may take a while to process, use\n    iterators to fetch on-demand).\n\n    else:\n        projects = gl.projects.list(iterator=True)\n        groups = gl.groups.list(iterator=True)\n\n    print(\"# List of all CI/CD variables marked as secret (instance, groups,\n    projects)\")\n\n\n    # https://python-gitlab.readthedocs.io/en/stable/gl_objects/variables.html\n\n\n    # Instance variables (if the token has permissions)\n\n    print(\"Instance variables, if accessible\")\n\n    try:\n        for i_var in gl.variables.list(iterator=True):\n            eval_print_var(i_var)\n    except:\n        print(\"No permission to fetch global instance variables, continueing without.\")\n        print(\"\\n\")\n\n    # group variables (maintainer permissions for groups required)\n\n    for group in groups:\n        print(\"Group {n}, URL: {u}\".format(n=group.full_path, u=group.web_url))\n        for g_var in group.variables.list(iterator=True):\n            eval_print_var(g_var)\n\n        print(\"\\n\")\n\n    # Loop over projects and print the settings\n\n    for project in projects:\n        # skip archived projects, they throw 403 errors\n        if project.archived:\n            continue\n\n        print(\"Project {n}, URL: {u}\".format(n=project.path_with_namespace, u=project.web_url))\n        for p_var in project.variables.list(iterator=True):\n            eval_print_var(p_var)\n\n        print(\"\\n\")\n\n    ```\n\n    Das Skript druckt die Variablenwerte absichtlich nicht aus; dies soll als\n    Übung für sichere Umgebungen dienen. Für die Speicherung von Daten empfiehlt\n    sich die Verwendung [externer\n    Anbieter](https://docs.gitlab.com/ci/secrets/).\n\n\n    ### Herunterladen einer Datei aus dem Repository\n\n\n    Ziel des\n    [Skripts](https://gitlab.com/gitlab-da/use-cases/gitlab-api/gitlab-api-python/-/blob/main/get_raw_file_content.py)\n    ist es, einen Dateipfad von einem angegebenen Verzweigungsnamen\n    herunterzuladen und dessen Inhalt in einer neuen Datei zu speichern.\n\n\n    ```python\n\n    # Goal: Try to download README.md from\n    https://gitlab.com/gitlab-da/use-cases/gitlab-api/gitlab-api-python/-/blob/main/README.md\n\n    FILE_NAME = 'README.md'\n\n    BRANCH_NAME = 'main'\n\n\n    # Search the file in the repository tree and get the raw blob\n\n    for f in project.repository_tree():\n        print(\"File path '{name}' with id '{id}'\".format(name=f['name'], id=f['id']))\n\n        if f['name'] == FILE_NAME:\n            f_content = project.repository_raw_blob(f['id'])\n            print(f_content)\n\n    # Alternative approach: Get the raw file from the main branch\n\n    raw_content = project.files.raw(file_path=FILE_NAME, ref=BRANCH_NAME)\n\n    print(raw_content)\n\n\n    # Store the file on disk\n\n    with open('raw_README.md', 'wb') as f:\n        project.files.raw(file_path=FILE_NAME, ref=BRANCH_NAME, streamed=True, action=f.write)\n\n    ```\n\n\n    ### Hilfe zur Migration: Auflistung aller zertifikatsbasierten\n    Kubernetes-Cluster\n\n\n    Die zertifikatsbasierte Integration von Kubernetes-Clustern in GitLab wurde\n    [abgeschafft](https://docs.gitlab.com/update/deprecations/#self-managed-certificate-based-integration-with-kubernetes).\n    Um Migrationspläne zu unterstützen, kann die Erfassung bestehender Gruppen\n    und Projekte mithilfe der GitLab-API automatisiert werden.\n\n\n    ```python\n\n    groups = [ ]\n\n\n    # get GROUP_ID group\n\n    groups.append(gl.groups.get(GROUP_ID))\n\n\n    for group in groups:\n        for sg in group.subgroups.list(include_subgroups=True, all=True):\n            real_group = gl.groups.get(sg.id)\n            groups.append(real_group)\n\n    group_clusters = {}\n\n    project_clusters = {}\n\n\n    for group in groups:\n        #Collect group clusters\n        g_clusters = group.clusters.list()\n\n        if len(g_clusters) > 0:\n            group_clusters[group.id] = g_clusters\n\n        # Collect all projects in group and subgroups and their clusters\n        projects = group.projects.list(include_subgroups=True, all=True)\n\n        for project in projects:\n            # https://python-gitlab.readthedocs.io/en/stable/gl_objects/groups.html#examples\n            manageable_project = gl.projects.get(project.id)\n\n            # skip archived projects\n            if project.archived:\n                continue\n\n            p_clusters = manageable_project.clusters.list()\n\n            if len(p_clusters) > 0:\n                project_clusters[project.id] = p_clusters\n\n    # Print summary\n\n    print(\"## Group clusters\\n\\n\")\n\n    for g_id, g_clusters in group_clusters.items():\n        url = gl.groups.get(g_id).web_url\n        print(\"Group ID {g_id}: {u}\\n\\n\".format(g_id=g_id, u=url))\n        print_clusters(g_clusters)\n\n    print(\"## Project clusters\\n\\n\")\n\n    for p_id, p_clusters in project_clusters.items():\n        url = gl.projects.get(p_id).web_url\n        print(\"Project ID {p_id}: {u}\\n\\n\".format(p_id=p_id, u=url))\n        print_clusters(p_clusters)\n\n    ```\n\n\n    Hier findest du das vollständige\n    [Skript](https://gitlab.com/gitlab-da/use-cases/gitlab-api/gitlab-api-python/-/blob/main/list_cert_based_kubernetes_clusters.py).\n\n\n    ### Team-Effizienz: Prüfe, ob bestehende Merge-Requests nach dem Mergen\n    einer großen Refactoring-MR neu gebasht werden müssen\n\n\n    Das [GitLab-Handbuch-Repository](https://handbook.gitlab.com/handbook/) ist\n    ein großes Monorepo mit zahlreichen Merge-Requests, die erstellt, geprüft,\n    genehmigt und zusammengeführt werden müssen. Einige Prüfungen dauern länger\n    als andere, insbesondere wenn Zusammenführungsanfragen mehrere Seiten\n    betreffen, beispielsweise wenn ein String umbenannt wird oder Änderungen auf\n    [alle](https://handbook.gitlab.com/handbook/about/#count-handbook-pages)\n    Handbuchseiten ausgedehnt werden. Das Marketing-Handbuch wurde\n    umstrukturiert, was zu vielen Verschiebungen oder Umbenennungen von\n    Verzeichnissen und Pfaden führte.\n\n\n    Mit der Zeit nahmen die Issues zu, und es bestand die Sorge, dass andere\n    Merge-Anfragen nach dem Zusammenführen der großen Änderungen auf Konflikte\n    stoßen könnten. Es wurde festgestellt, dass Python-Gitlab in der Lage ist,\n    alle Merge-Requests in einem bestimmten Projekt abzurufen, einschließlich\n    Details über den Git-Zweig, geänderte Quellpfade und vieles mehr.\n\n\n    Das daraus resultierende Skript konfiguriert eine Liste von Quellpfaden, die\n    von allen pythongitlab-Merge-Requests berührt werden, und vergleicht die\n    Diffs der Merge-Requests mit mr.diffs.list(), um festzustellen, ob ein\n    Muster mit dem Wert in old_path. übereinstimmt. Bei einer Übereinstimmung\n    protokolliert das Skript diese und speichert die Zusammenführungsanforderung\n    im seen_mr-Wörterbuch für die spätere Zusammenfassung. Zusätzlich werden\n    Attribute gesammelt, um eine Markdown-Aufgabenliste mit URLs zum leichteren\n    Einfügen in Issue-Beschreibungen zu erstellen.\n\n\n    ```python\n\n    PATH_PATTERNS = [\n        'path/to/handbook/source/page.md',\n    ]\n\n\n    # Only list opened MRs\n\n    #\n    https://python-gitlab.readthedocs.io/en/stable/gl_objects/merge_requests.html#project-merge-requests\n\n    mrs = project.mergerequests.list(state='opened', iterator=True)\n\n\n    seen_mr = {}\n\n\n    for mr in mrs:\n        # https://docs.gitlab.com/api/merge_requests/#list-merge-request-diffs\n        real_mr = project.mergerequests.get(mr.get_id())\n        real_mr_id = real_mr.attributes['iid']\n        real_mr_url = real_mr.attributes['web_url']\n\n        for diff in real_mr.diffs.list(iterator=True):\n            real_diff = real_mr.diffs.get(diff.id)\n\n            for d in real_diff.attributes['diffs']:\n                for p in PATH_PATTERNS:\n                    if p in d['old_path']:\n                        print(\"MATCH: {p} in MR {mr_id}, status '{s}', title '{t}' - URL: {mr_url}\".format(\n                            p=p,\n                            mr_id=real_mr_id,\n                            s=mr_status,\n                            t=real_mr.attributes['title'],\n                            mr_url=real_mr_url))\n\n                        if not real_mr_id in seen_mr:\n                            seen_mr[real_mr_id] = real_mr\n\n    print(\"\\n# MRs to update\\n\")\n\n\n    for id, real_mr in seen_mr.items():\n        print(\"- [ ] !{mr_id} - {mr_url}+ Status: {s}, Title: {t}\".format(\n            mr_id=id,\n            mr_url=real_mr.attributes['web_url'],\n            s=real_mr.attributes['detailed_merge_status'],\n            t=real_mr.attributes['title']))\n\n    ```\n\n\n    ## DevSecOps-Anwendungsfälle für API-Schreibaktionen\n\n\n    Das authentifizierte Zugriffstoken benötigt den vollen Anwendungsbereich der\n    [`api`](https://docs.gitlab.com/user/profile/personal_access_tokens/#personal-access-token-scopes).\n\n\n    Die folgenden Anwendungsfälle werden diskutiert:\n\n\n    - Verschieben von Epics zwischen Gruppen\n\n    - Compliance: Sicherstellen, dass Projekteinstellungen nicht überschrieben\n    werden\n\n    - Notizen machen, Fälligkeitsübersicht erstellen\n\n\n    ### Verschieben von Epics zwischen Gruppen\n\n\n    Manchmal ist es erforderlich, Epics, ähnlich wie Issues, in eine andere\n    Gruppe zu verschieben. Eine Frage im GitLab-Marketing-Slack-Kanal hat dazu\n    geführt, einen\n    [Funktionsvorschlag](https://gitlab.com/gitlab-org/gitlab/-/issues/12689)\n    für die Benutzeroberfläche und die\n    [Schnellaktionen](/blog/improve-your-gitlab-productivity-with-these-10-tips/)\n    zu prüfen und später über das Schreiben eines API-Skripts nachzudenken, um\n    die Schritte zu automatisieren.\n\n\n    ### Automatisierung des Verschiebens von Epics\n\n\n    Die Idee ist einfach: Ein Epic wird von einer Quellgruppe in eine Zielgruppe\n    verschoben, wobei Titel, Beschreibung und Labels kopiert werden. Da Epics es\n    erlauben, Themen zu gruppieren, müssen sie auch dem Ziel-Epic neu zugewiesen\n    werden. Parent-Child-Epic-Relationships müssen dabei berücksichtigt werden:\n    Alle Child-Epics der Quell-Epics müssen dem Ziel-Epic neu zugewiesen werden.\n\n\n    Das folgende Skript sucht zunächst alle\n    [Attribute](https://python-gitlab.readthedocs.io/en/stable/gl_objects/epics.html)\n    des Quellepos und erstellt dann ein neues Zielepos mit den minimalen\n    Attributen: Titel und Beschreibung. Die Liste der Bezeichnungen wird kopiert\n    und die Änderungen werden mit dem save()-Aufruf beibehalten. Die Ausgaben,\n    die dem Epos zugeordnet sind, müssen im Zielepos neu erstellt werden.\n\n\n    Der create()-Aufruf erzeugt das Beziehungselement und nicht ein neues\n    Issue-Objekt selbst. Das Verschieben von Child-Epics erfordert einen anderen\n    Ansatz, da die Beziehung umgekehrt ist: Die parent_id des Child-Epics muss\n    mit der ID des Quell-Epics verglichen und bei Übereinstimmung auf die ID des\n    Ziel-Epics aktualisiert werden. Nachdem alles erfolgreich kopiert wurde,\n    muss das Quell-Epos in den closed-Zustand versetzt werden.\n\n\n    ```python\n\n    #!/usr/bin/env python\n\n\n    # Description: Show how epics can be moved between groups, including title,\n    description, labels, child epics and issues.\n\n    # Requirements: python-gitlab Python libraries. GitLab API write access, and\n    maintainer access to all configured groups/projects.\n\n    # Author: Michael Friedrich \u003Cmfriedrich@gitlab.com>\n\n    # License: MIT, (c) 2023-present GitLab B.V.\n\n\n    import gitlab\n\n    import os\n\n    import sys\n\n\n    GITLAB_SERVER = os.environ.get('GL_SERVER', 'https://gitlab.com')\n\n    # https://gitlab.com/gitlab-da/use-cases/gitlab-api\n\n    SOURCE_GROUP_ID = os.environ.get('GL_SOURCE_GROUP_ID', 62378643)\n\n    # https://gitlab.com/gitlab-da/use-cases/gitlab-api/epic-move-target\n\n    TARGET_GROUP_ID = os.environ.get('GL_TARGET_GROUP_ID', 62742177)\n\n    # https://gitlab.com/groups/gitlab-da/use-cases/gitlab-api/-/epics/1\n\n    EPIC_ID = os.environ.get('GL_EPIC_ID', 1)\n\n    GITLAB_TOKEN = os.environ.get('GL_TOKEN')\n\n\n    if not GITLAB_TOKEN:\n        print(\"Please set the GL_TOKEN env variable.\")\n        sys.exit(1)\n\n    gl = gitlab.Gitlab(GITLAB_SERVER, private_token=GITLAB_TOKEN)\n\n\n    # Main\n\n    # Goal: Move epic to target group, including title, body, labels, and child\n    epics and issues.\n\n    source_group = gl.groups.get(SOURCE_GROUP_ID)\n\n    target_group = gl.groups.get(TARGET_GROUP_ID)\n\n\n    # Create a new target epic and copy all its items, then close the source\n    epic.\n\n    source_epic = source_group.epics.get(EPIC_ID)\n\n    # print(source_epic) #debug\n\n\n    epic_title = source_epic.title\n\n    epic_description = source_epic.description\n\n    epic_labels = source_epic.labels\n\n    epic_issues = source_epic.issues.list()\n\n\n    # Create the epic with minimal attributes\n\n    target_epic = target_group.epics.create({\n        'title': epic_title,\n        'description': epic_description,\n    })\n\n\n    # Assign the list\n\n    target_epic.labels = epic_labels\n\n\n    # Persist the changes in the new epic\n\n    target_epic.save()\n\n\n    # Epic issues need to be re-assigned in a loop\n\n    for epic_issue in epic_issues:\n        ei = target_epic.issues.create({'issue_id': epic_issue.id})\n\n    # Child epics need to update their parent_id to the new epic\n\n    # Need to search in all epics, use lazy object loading\n\n    for sge in source_group.epics.list(lazy=True):\n        # this epic has the source epic as parent epic?\n        if sge.parent_id == source_epic.id:\n            # Update the parent id\n            sge.parent_id = target_epic.id\n            sge.save()\n\n    print(\"Copied source epic {source_id} ({source_url}) to target epic\n    {target_id} ({target_url})\".format(\n        source_id=source_epic.id, source_url=source_epic.web_url,\n        target_id=target_epic.id, target_url=target_epic.web_url))\n\n    # Close the old epic\n\n    source_epic.state_event = 'close'\n\n    source_epic.save()\n\n    print(\"Closed source epic {source_id} ({source_url})\".format(\n        source_id=source_epic.id, source_url=source_epic.web_url))\n\n    ```\n\n\n    ```shell\n\n    $  python3 move_epic_between_groups.py\n\n    Copied source epic 725341\n    (https://gitlab.com/groups/gitlab-da/use-cases/gitlab-api/-/epics/1) to\n    target epic 725358\n    (https://gitlab.com/groups/gitlab-da/use-cases/gitlab-api/epic-move-target/-/epics/6)\n\n    Closed source epic 725341\n    (https://gitlab.com/groups/gitlab-da/use-cases/gitlab-api/-/epics/1)\n\n    ```\n\n\n    Das\n    [Ziel-Epic](https://gitlab.com/groups/gitlab-da/use-cases/gitlab-api/epic-move-target/-/epics/5)\n    wurde erstellt und zeigt das erwartete Ergebnis: Derselbe Titel, dieselbe\n    Beschreibung, dieselben Bezeichnungen, dasselbe untergeordnete Epic und\n    dieselben Issues.\n\n\n    ![Target epic which has all attributes copied from the source epic: title,\n    description, labels, child epics,\n    issues](https://about.gitlab.com/images/blogimages/efficient-devsecops-workflows-python-gitlab-handson/python_gitlab_moved_epic_with_all_attributes.png){:\n    .shadow}\n\n\n    **Übung:** Das Skript kopiert noch keine\n    [Kommentare](https://python-gitlab.readthedocs.io/en/stable/gl_objects/notes.html)\n    und\n    [Diskussionsstränge](https://python-gitlab.readthedocs.io/en/stable/gl_objects/discussions.html).\n    Recherchiere und hilf mit, das Skript zu aktualisieren – Merge-Requests\n    willkommen!\n\n\n    ### Compliance: Sicherstellen, dass Projekteinstellungen nicht überschrieben\n    werden\n\n\n    Projekt- und Gruppeneinstellungen können versehentlich von Teammitgliedern\n    mit Administratorrechten geändert werden. Die Compliance-Anforderungen\n    müssen erfüllt werden. Ein weiterer Anwendungsfall ist die Verwaltung der\n    Konfiguration mit Infrastructure-as-Code-Tools, um sicherzustellen, dass die\n    Konfiguration von GitLab-Instanzen, -Gruppen, -Projekten usw. erhalten\n    bleibt und immer dieselbe ist. Tools wie Ansible oder Terraform können ein\n    API-Skript aufrufen oder die Python-GitLab-Bibliothek verwenden, um Aufgaben\n    zur Verwaltung von Einstellungen auszuführen.\n\n\n    Im folgenden Beispiel ist nur der main-Zweig geschützt.\n\n\n    ![GitLab project settings for repositories and protected branches, main\n    branch](https://about.gitlab.com/images/blogimages/efficient-devsecops-workflows-python-gitlab-handson/python_gitlab_protected_branches_settings_main.png){:\n    .shadow}\n\n\n    Nehmen wir an, dass ein neuer production-Zweig hinzugefügt wurde und\n    ebenfalls geschützt werden soll. Das folgende\n    [Skript](https://gitlab.com/gitlab-da/use-cases/gitlab-api/gitlab-api-python/-/blob/main/enforce_protected_branches.py)\n    definiert das Wörterbuch der geschützten Zweige und ihre Zugriffsebenen für\n    Push-/Merge-Berechtigungen auf Maintainer-Ebene und baut die Vergleichslogik\n    auf der Grundlage der\n    [python-gitlab-Dokumentation](https://python-gitlab.readthedocs.io/en/stable/gl_objects/protected_branches.html)\n    zu geschützten Zweigen auf.\n\n\n    ```python\n\n    #!/usr/bin/env python\n\n\n    import gitlab\n\n    import os\n\n    import sys\n\n\n    GITLAB_SERVER = os.environ.get('GL_SERVER', 'https://gitlab.com')\n\n    # https://gitlab.com/gitlab-da/use-cases/\n\n    GROUP_ID = os.environ.get('GL_GROUP_ID', 16058698)\n\n    GITLAB_TOKEN = os.environ.get('GL_TOKEN')\n\n\n    PROTECTED_BRANCHES = {\n        'main': {\n            'merge_access_level': gitlab.const.AccessLevel.MAINTAINER,\n            'push_access_level': gitlab.const.AccessLevel.MAINTAINER\n        },\n        'production': {\n            'merge_access_level': gitlab.const.AccessLevel.MAINTAINER,\n            'push_access_level': gitlab.const.AccessLevel.MAINTAINER\n        },\n    }\n\n\n    if not GITLAB_TOKEN:\n        print(\"Please set the GL_TOKEN env variable.\")\n        sys.exit(1)\n\n    gl = gitlab.Gitlab(GITLAB_SERVER, private_token=GITLAB_TOKEN)\n\n\n    # Main\n\n    group = gl.groups.get(GROUP_ID)\n\n\n    # Collect all projects in group and subgroups\n\n    projects = group.projects.list(include_subgroups=True, all=True)\n\n\n    for project in projects:\n        # Retrieve a full manageable project object\n        # https://python-gitlab.readthedocs.io/en/stable/gl_objects/groups.html#examples\n        manageable_project = gl.projects.get(project.id)\n\n        # https://python-gitlab.readthedocs.io/en/stable/gl_objects/protected_branches.html\n        protected_branch_names = []\n\n        for pb in manageable_project.protectedbranches.list():\n            manageable_protected_branch = manageable_project.protectedbranches.get(pb.name)\n            print(\"Protected branch name: {n}, merge_access_level: {mal}, push_access_level: {pal}\".format(\n                n=manageable_protected_branch.name,\n                mal=manageable_protected_branch.merge_access_levels,\n                pal=manageable_protected_branch.push_access_levels\n            ))\n\n            protected_branch_names.append(manageable_protected_branch.name)\n\n        for branch_to_protect, levels in PROTECTED_BRANCHES.items():\n            # Fix missing protected branches\n            if branch_to_protect not in protected_branch_names:\n                print(\"Adding branch {n} to protected branches settings\".format(n=branch_to_protect))\n                p_branch = manageable_project.protectedbranches.create({\n                    'name': branch_to_protect,\n                    'merge_access_level': gitlab.const.AccessLevel.MAINTAINER,\n                    'push_access_level': gitlab.const.AccessLevel.MAINTAINER\n                })\n\n    ```\n\n\n    Wenn das Skript ausgeführt wird, werden der bestehende main-Zweig und ein\n    Hinweis, dass die production aktualisiert wird, ausgegeben. Der Screenshot\n    aus den Repository-Einstellungen verdeutlicht diese Aktion.\n\n\n    ```shell\n\n    $ python3 enforce_protected_branches.py ─╯\n\n    Protected branch name: main, merge_access_level: [{'id': 67294702,\n    'access_level': 40, 'access_level_description': 'Maintainers', 'user_id':\n    None, 'group_id': None}], push_access_level: [{'id': 68546039,\n    'access_level': 40, 'access_level_description': 'Maintainers', 'user_id':\n    None, 'group_id': None}]\n\n    Adding branch production to protected branches settings\n\n    ```\n\n\n    ![GitLab project settings for repositories and protected branches, main and\n    production\n    branch](https://about.gitlab.com/images/blogimages/efficient-devsecops-workflows-python-gitlab-handson/python_gitlab_protected_branches_settings_main_production.png){:\n    .shadow}\n\n\n    ### Notizen machen, Fälligkeitsübersicht erstellen\n\n\n    Eine Diskussion auf [Hacker\n    News](https://news.ycombinator.com/item?id=32155848) über Tools zum\n    Erstellen von Notizen hat mich dazu inspiriert, eine Übersicht in Form einer\n    Markdown-Tabelle zu erstellen, die aus Dateien, die Notizen aufnehmen,\n    geholt und nach dem geparsten Fälligkeitsdatum sortiert wird. Das\n    [Skript](https://gitlab.com/gitlab-da/use-cases/gitlab-api/gitlab-api-python/-/blob/main/generate_snippets_index_by_due_date.py)\n    ist hier zu finden und etwas komplexer zu verstehen.\n\n\n    \u003C!--\n\n    # 2022-07-19 Notes\n\n\n    HN topic about taking notes: https://news.ycombinator.com/item?id=32152935\n\n\n    -->\n\n\n    ***Die englischsprachige\n    [Originalversion](https://about.gitlab.com/blog/efficient-devsecops-workflows-hands-on-python-gitlab-api-automation/)\n    dieses Artikels wurde bereits aktualisiert und erhält einige weitere Tipps,\n    welche wir der deutschen Version beizeiten hinzufügen werden.***\n  category: engineering\n  tags:\n    - integrations\n    - tutorial\n    - DevSecOps\n    - DevSecOps platform\n  updatedDate: '2025-05-16'\nconfig:\n  slug: efficient-devsecops-workflows-hands-on-python-gitlab-api-automation\n  featured: false\n  template: BlogPost\n",{"title":5,"description":17,"ogTitle":5,"ogDescription":17,"noIndex":14,"ogImage":19,"ogUrl":34,"ogSiteName":35,"ogType":36,"canonicalUrls":34},"https://about.gitlab.com/blog/efficient-devsecops-workflows-hands-on-python-gitlab-api-automation","https://about.gitlab.com","article","de-de/blog/efficient-devsecops-workflows-hands-on-python-gitlab-api-automation",[22,23,39,40],"devsecops","devsecops-platform",[22,23,24,25],"OMcbYZnnAM9G6VdusR8FKtJbnb7TrPs4o3iqD5HeE2Y",{"data":44},{"logo":45,"freeTrial":50,"sales":55,"login":60,"items":65,"search":374,"minimal":408,"duo":426,"switchNav":435,"pricingDeployment":446},{"config":46},{"href":47,"dataGaName":48,"dataGaLocation":49},"/de-de/","gitlab logo","header",{"text":51,"config":52},"Kostenlose Testversion anfordern",{"href":53,"dataGaName":54,"dataGaLocation":49},"https://gitlab.com/-/trial_registrations/new?glm_source=about.gitlab.com/de-de&glm_content=default-saas-trial/","free trial",{"text":56,"config":57},"Vertrieb kontaktieren",{"href":58,"dataGaName":59,"dataGaLocation":49},"/de-de/sales/","sales",{"text":61,"config":62},"Anmelden",{"href":63,"dataGaName":64,"dataGaLocation":49},"https://gitlab.com/users/sign_in/","sign in",[66,93,190,195,295,355],{"text":67,"config":68,"cards":70},"Plattform",{"dataNavLevelOne":69},"platform",[71,77,85],{"title":67,"description":72,"link":73},"Die intelligente Orchestrierungsplattform für DevSecOps",{"text":74,"config":75},"Die Plattform erkunden",{"href":76,"dataGaName":69,"dataGaLocation":49},"/de-de/platform/",{"title":78,"description":79,"link":80},"GitLab Duo Agent Platform","Agentische KI für den gesamten Software-Lebenszyklus",{"text":81,"config":82},"Lerne GitLab Duo kennen",{"href":83,"dataGaName":84,"dataGaLocation":49},"/de-de/gitlab-duo-agent-platform/","gitlab duo agent platform",{"title":86,"description":87,"link":88},"Warum GitLab?","Erfahre, warum sich Unternehmen für GitLab entscheiden",{"text":89,"config":90},"Mehr erfahren",{"href":91,"dataGaName":92,"dataGaLocation":49},"/de-de/why-gitlab/","why gitlab",{"text":94,"left":30,"config":95,"link":97,"lists":101,"footer":172},"Produkt",{"dataNavLevelOne":96},"solutions",{"text":98,"config":99},"Alle Lösungen anzeigen",{"href":100,"dataGaName":96,"dataGaLocation":49},"/de-de/solutions/",[102,127,150],{"title":103,"description":104,"link":105,"items":110},"Automatisierung","CI/CD und Automatisierung zur Beschleunigung der Bereitstellung",{"config":106},{"icon":107,"href":108,"dataGaName":109,"dataGaLocation":49},"AutomatedCodeAlt","/de-de/solutions/delivery-automation/","automated software delivery",[111,115,118,123],{"text":112,"config":113},"CI/CD",{"href":114,"dataGaLocation":49,"dataGaName":112},"/de-de/solutions/continuous-integration/",{"text":78,"config":116},{"href":83,"dataGaLocation":49,"dataGaName":117},"gitlab duo agent platform - product menu",{"text":119,"config":120},"Quellcodeverwaltung",{"href":121,"dataGaLocation":49,"dataGaName":122},"/de-de/solutions/source-code-management/","Source Code Management",{"text":124,"config":125},"Automatische Softwarebereitstellung",{"href":108,"dataGaLocation":49,"dataGaName":126},"Automated software delivery",{"title":128,"description":129,"link":130,"items":135},"Sicherheit","Entwickle Code schneller ohne Abstriche bei der Sicherheit",{"config":131},{"href":132,"dataGaName":133,"dataGaLocation":49,"icon":134},"/de-de/solutions/application-security-testing/","security and compliance","ShieldCheckLight",[136,140,145],{"text":137,"config":138},"Anwendungssicherheitstests",{"href":132,"dataGaName":139,"dataGaLocation":49},"Application security testing",{"text":141,"config":142},"Schutz der Software-Lieferkette",{"href":143,"dataGaLocation":49,"dataGaName":144},"/de-de/solutions/supply-chain/","Software supply chain security",{"text":146,"config":147},"Software-Compliance",{"href":148,"dataGaName":149,"dataGaLocation":49},"/de-de/solutions/software-compliance/","software compliance",{"title":151,"link":152,"items":157},"Auswertung",{"config":153},{"icon":154,"href":155,"dataGaName":156,"dataGaLocation":49},"DigitalTransformation","/de-de/solutions/visibility-measurement/","visibility and measurement",[158,162,167],{"text":159,"config":160},"Sichtbarkeit und Auswertung",{"href":155,"dataGaLocation":49,"dataGaName":161},"Visibility and Measurement",{"text":163,"config":164},"Wertstrommanagement",{"href":165,"dataGaLocation":49,"dataGaName":166},"/de-de/solutions/value-stream-management/","Value Stream Management",{"text":168,"config":169},"Analysen und Einblicke",{"href":170,"dataGaLocation":49,"dataGaName":171},"/de-de/solutions/analytics-and-insights/","Analytics and insights",{"title":173,"items":174},"GitLab für",[175,180,185],{"text":176,"config":177},"Enterprise",{"href":178,"dataGaLocation":49,"dataGaName":179},"/de-de/enterprise/","enterprise",{"text":181,"config":182},"Kleinunternehmen",{"href":183,"dataGaLocation":49,"dataGaName":184},"/de-de/small-business/","small business",{"text":186,"config":187},"Öffentlicher Sektor",{"href":188,"dataGaLocation":49,"dataGaName":189},"/de-de/solutions/public-sector/","public sector",{"text":191,"config":192},"Preise",{"href":193,"dataGaName":194,"dataGaLocation":49,"dataNavLevelOne":194},"/de-de/pricing/","pricing",{"text":196,"config":197,"link":199,"lists":203,"feature":282},"Ressourcen",{"dataNavLevelOne":198},"resources",{"text":200,"config":201},"Alle Ressourcen anzeigen",{"href":202,"dataGaName":198,"dataGaLocation":49},"/de-de/resources/",[204,236,254],{"title":205,"items":206},"Erste Schritte",[207,212,217,222,227,232],{"text":208,"config":209},"Installieren",{"href":210,"dataGaName":211,"dataGaLocation":49},"/de-de/install/","install",{"text":213,"config":214},"Kurzanleitungen",{"href":215,"dataGaName":216,"dataGaLocation":49},"/de-de/get-started/","quick setup checklists",{"text":218,"config":219},"Lernen",{"href":220,"dataGaLocation":49,"dataGaName":221},"https://university.gitlab.com/","learn",{"text":223,"config":224},"Produktdokumentation",{"href":225,"dataGaName":226,"dataGaLocation":49},"https://docs.gitlab.com/","product documentation",{"text":228,"config":229},"Best-Practice-Videos",{"href":230,"dataGaName":231,"dataGaLocation":49},"/de-de/getting-started-videos/","best practice videos",{"text":233,"config":234},"Integrationen",{"href":235,"dataGaName":22,"dataGaLocation":49},"/de-de/integrations/",{"title":237,"items":238},"Entdecken",[239,244,249],{"text":240,"config":241},"Kundenerfolge",{"href":242,"dataGaName":243,"dataGaLocation":49},"/de-de/customers/","customer success stories",{"text":245,"config":246},"Blog",{"href":247,"dataGaName":248,"dataGaLocation":49},"/de-de/blog/","blog",{"text":250,"config":251},"Remote",{"href":252,"dataGaName":253,"dataGaLocation":49},"https://handbook.gitlab.com/handbook/company/culture/all-remote/","remote",{"title":255,"items":256},"Vernetzen",[257,262,267,272,277],{"text":258,"config":259},"GitLab Services",{"href":260,"dataGaName":261,"dataGaLocation":49},"/de-de/services/","services",{"text":263,"config":264},"Community",{"href":265,"dataGaName":266,"dataGaLocation":49},"/community/","community",{"text":268,"config":269},"Forum",{"href":270,"dataGaName":271,"dataGaLocation":49},"https://forum.gitlab.com/","forum",{"text":273,"config":274},"Veranstaltungen",{"href":275,"dataGaName":276,"dataGaLocation":49},"/events/","events",{"text":278,"config":279},"Partner",{"href":280,"dataGaName":281,"dataGaLocation":49},"/de-de/partners/","partners",{"background":283,"textColor":284,"text":285,"image":286,"link":290},"#2f2a6b","#fff","Perspektiven für die Softwareentwicklung der Zukunft",{"altText":287,"config":288},"The Source Promo-Karte",{"src":289},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1758208064/dzl0dbift9xdizyelkk4.svg",{"text":291,"config":292},"Aktuelles",{"href":293,"dataGaName":294,"dataGaLocation":49},"/de-de/the-source/","the source",{"text":296,"config":297,"lists":299},"Unternehmen",{"dataNavLevelOne":298},"company",[300],{"items":301},[302,307,313,315,320,325,330,335,340,345,350],{"text":303,"config":304},"Über",{"href":305,"dataGaName":306,"dataGaLocation":49},"/de-de/company/","about",{"text":308,"config":309,"footerGa":312},"Karriere",{"href":310,"dataGaName":311,"dataGaLocation":49},"/jobs/","jobs",{"dataGaName":311},{"text":273,"config":314},{"href":275,"dataGaName":276,"dataGaLocation":49},{"text":316,"config":317},"Geschäftsführung",{"href":318,"dataGaName":319,"dataGaLocation":49},"/company/team/e-group/","leadership",{"text":321,"config":322},"Team",{"href":323,"dataGaName":324,"dataGaLocation":49},"/company/team/","team",{"text":326,"config":327},"Handbuch",{"href":328,"dataGaName":329,"dataGaLocation":49},"https://handbook.gitlab.com/","handbook",{"text":331,"config":332},"Investor Relations",{"href":333,"dataGaName":334,"dataGaLocation":49},"https://ir.gitlab.com/","investor relations",{"text":336,"config":337},"Trust Center",{"href":338,"dataGaName":339,"dataGaLocation":49},"/de-de/security/","trust center",{"text":341,"config":342},"AI Transparency Center",{"href":343,"dataGaName":344,"dataGaLocation":49},"/de-de/ai-transparency-center/","ai transparency center",{"text":346,"config":347},"Newsletter",{"href":348,"dataGaName":349,"dataGaLocation":49},"/company/contact/#contact-forms","newsletter",{"text":351,"config":352},"Presse",{"href":353,"dataGaName":354,"dataGaLocation":49},"/press/","press",{"text":356,"config":357,"lists":358},"Kontakt",{"dataNavLevelOne":298},[359],{"items":360},[361,364,369],{"text":56,"config":362},{"href":58,"dataGaName":363,"dataGaLocation":49},"talk to sales",{"text":365,"config":366},"Support-Portal",{"href":367,"dataGaName":368,"dataGaLocation":49},"https://support.gitlab.com","support portal",{"text":370,"config":371},"Kundenportal",{"href":372,"dataGaName":373,"dataGaLocation":49},"https://customers.gitlab.com/customers/sign_in/","customer portal",{"close":375,"login":376,"suggestions":383},"Schließen",{"text":377,"link":378},"Um Repositorys und Projekte zu durchsuchen, melde dich an bei",{"text":379,"config":380},"gitlab.com",{"href":63,"dataGaName":381,"dataGaLocation":382},"search login","search",{"text":384,"default":385},"Vorschläge",[386,388,393,395,400,405],{"text":78,"config":387},{"href":83,"dataGaName":78,"dataGaLocation":382},{"text":389,"config":390},"Codevorschläge (KI)",{"href":391,"dataGaName":392,"dataGaLocation":382},"/de-de/solutions/code-suggestions/","Code Suggestions (AI)",{"text":112,"config":394},{"href":114,"dataGaName":112,"dataGaLocation":382},{"text":396,"config":397},"GitLab auf AWS",{"href":398,"dataGaName":399,"dataGaLocation":382},"/de-de/partners/technology-partners/aws/","GitLab on AWS",{"text":401,"config":402},"GitLab auf Google Cloud",{"href":403,"dataGaName":404,"dataGaLocation":382},"/de-de/partners/technology-partners/google-cloud-platform/","GitLab on Google Cloud",{"text":86,"config":406},{"href":91,"dataGaName":407,"dataGaLocation":382},"Why GitLab?",{"freeTrial":409,"mobileIcon":414,"desktopIcon":419,"secondaryButton":422},{"text":410,"config":411},"Kostenlos testen",{"href":412,"dataGaName":54,"dataGaLocation":413},"https://gitlab.com/-/trials/new/","nav",{"altText":415,"config":416},"GitLab-Symbol",{"src":417,"dataGaName":418,"dataGaLocation":413},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1758203874/jypbw1jx72aexsoohd7x.svg","gitlab icon",{"altText":415,"config":420},{"src":421,"dataGaName":418,"dataGaLocation":413},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1758203875/gs4c8p8opsgvflgkswz9.svg",{"text":205,"config":423},{"href":424,"dataGaName":425,"dataGaLocation":413},"https://gitlab.com/-/trial_registrations/new?glm_source=about.gitlab.com/de-de/get-started/","get started",{"freeTrial":427,"mobileIcon":431,"desktopIcon":433},{"text":428,"config":429},"Mehr über GitLab Duo erfahren",{"href":83,"dataGaName":430,"dataGaLocation":413},"gitlab duo",{"altText":415,"config":432},{"src":417,"dataGaName":418,"dataGaLocation":413},{"altText":415,"config":434},{"src":421,"dataGaName":418,"dataGaLocation":413},{"button":436,"mobileIcon":441,"desktopIcon":443},{"text":437,"config":438},"/Option",{"href":439,"dataGaName":440,"dataGaLocation":413},"#contact","switch",{"altText":415,"config":442},{"src":417,"dataGaName":418,"dataGaLocation":413},{"altText":415,"config":444},{"src":445,"dataGaName":418,"dataGaLocation":413},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1773335277/ohhpiuoxoldryzrnhfrh.png",{"freeTrial":447,"mobileIcon":452,"desktopIcon":454},{"text":448,"config":449},"Zurück zur Preisübersicht",{"href":193,"dataGaName":450,"dataGaLocation":413,"icon":451},"back to pricing","GoBack",{"altText":415,"config":453},{"src":417,"dataGaName":418,"dataGaLocation":413},{"altText":415,"config":455},{"src":421,"dataGaName":418,"dataGaLocation":413},{"title":457,"button":458,"config":463},"Sieh dir an, wie agentische KI die Softwarebereitstellung transformiert",{"text":459,"config":460},"GitLab Transcend jetzt ansehen",{"href":461,"dataGaName":462,"dataGaLocation":49},"/de-de/events/transcend/virtual/","transcend event",{"layout":464,"icon":465,"disabled":30},"release","AiStar",{"data":467},{"text":468,"source":469,"edit":475,"contribute":480,"config":485,"items":490,"minimal":691},"Git ist eine Marke von Software Freedom Conservancy und unsere Verwendung von „GitLab“ erfolgt unter Lizenz.",{"text":470,"config":471},"Quelltext der Seite anzeigen",{"href":472,"dataGaName":473,"dataGaLocation":474},"https://gitlab.com/gitlab-com/marketing/digital-experience/about-gitlab-com/","page source","footer",{"text":476,"config":477},"Diese Seite bearbeiten",{"href":478,"dataGaName":479,"dataGaLocation":474},"https://gitlab.com/gitlab-com/marketing/digital-experience/about-gitlab-com/-/blob/main/content/","web ide",{"text":481,"config":482},"Beteilige dich",{"href":483,"dataGaName":484,"dataGaLocation":474},"https://gitlab.com/gitlab-com/marketing/digital-experience/about-gitlab-com/-/blob/main/CONTRIBUTING.md/","please contribute",{"twitter":486,"facebook":487,"youtube":488,"linkedin":489},"https://x.com/gitlab","https://www.facebook.com/gitlab","https://www.youtube.com/channel/UCnMGQ8QHMAnVIsI3xJrihhg","https://www.linkedin.com/company/gitlab-com",[491,536,587,629,656],{"title":191,"links":492,"subMenu":507},[493,497,502],{"text":494,"config":495},"Tarife anzeigen",{"href":193,"dataGaName":496,"dataGaLocation":474},"view plans",{"text":498,"config":499},"Vorteile von Premium",{"href":500,"dataGaName":501,"dataGaLocation":474},"/de-de/pricing/premium/","why premium",{"text":503,"config":504},"Vorteile von Ultimate",{"href":505,"dataGaName":506,"dataGaLocation":474},"/de-de/pricing/ultimate/","why ultimate",[508],{"title":356,"links":509},[510,512,514,516,521,526,531],{"text":56,"config":511},{"href":58,"dataGaName":59,"dataGaLocation":474},{"text":365,"config":513},{"href":367,"dataGaName":368,"dataGaLocation":474},{"text":370,"config":515},{"href":372,"dataGaName":373,"dataGaLocation":474},{"text":517,"config":518},"Status",{"href":519,"dataGaName":520,"dataGaLocation":474},"https://status.gitlab.com/","status",{"text":522,"config":523},"Nutzungsbedingungen",{"href":524,"dataGaName":525,"dataGaLocation":474},"/terms/","terms of use",{"text":527,"config":528},"Datenschutzerklärung",{"href":529,"dataGaName":530,"dataGaLocation":474},"/de-de/privacy/","privacy statement",{"text":532,"config":533},"Cookie-Einstellungen",{"dataGaName":534,"dataGaLocation":474,"id":535,"isOneTrustButton":30},"cookie preferences","ot-sdk-btn",{"title":94,"links":537,"subMenu":546},[538,542],{"text":539,"config":540},"DevSecOps-Plattform",{"href":76,"dataGaName":541,"dataGaLocation":474},"devsecops platform",{"text":543,"config":544},"KI-unterstützte Entwicklung",{"href":83,"dataGaName":545,"dataGaLocation":474},"ai-assisted development",[547],{"title":548,"links":549},"Themen",[550,554,559,564,569,572,577,582],{"text":112,"config":551},{"href":552,"dataGaName":553,"dataGaLocation":474},"/de-de/topics/ci-cd/","cicd",{"text":555,"config":556},"GitOps",{"href":557,"dataGaName":558,"dataGaLocation":474},"/de-de/topics/gitops/","gitops",{"text":560,"config":561},"DevOps",{"href":562,"dataGaName":563,"dataGaLocation":474},"/de-de/topics/devops/","devops",{"text":565,"config":566},"Versionskontrolle",{"href":567,"dataGaName":568,"dataGaLocation":474},"/de-de/topics/version-control/","version control",{"text":24,"config":570},{"href":571,"dataGaName":39,"dataGaLocation":474},"/de-de/topics/devsecops/",{"text":573,"config":574},"Cloud-nativ",{"href":575,"dataGaName":576,"dataGaLocation":474},"/de-de/topics/cloud-native/","cloud native",{"text":578,"config":579},"KI für das Programmieren",{"href":580,"dataGaName":581,"dataGaLocation":474},"/de-de/topics/devops/ai-for-coding/","ai for coding",{"text":583,"config":584},"Agentische KI",{"href":585,"dataGaName":586,"dataGaLocation":474},"/de-de/topics/agentic-ai/","agentic ai",{"title":588,"links":589},"Lösungen",[590,593,595,600,604,607,610,613,615,617,619,624],{"text":137,"config":591},{"href":132,"dataGaName":592,"dataGaLocation":474},"Application Security Testing",{"text":124,"config":594},{"href":108,"dataGaName":109,"dataGaLocation":474},{"text":596,"config":597},"Agile Entwicklung",{"href":598,"dataGaName":599,"dataGaLocation":474},"/de-de/solutions/agile-delivery/","agile delivery",{"text":601,"config":602},"SCM",{"href":121,"dataGaName":603,"dataGaLocation":474},"source code management",{"text":112,"config":605},{"href":114,"dataGaName":606,"dataGaLocation":474},"continuous integration & delivery",{"text":163,"config":608},{"href":165,"dataGaName":609,"dataGaLocation":474},"value stream management",{"text":555,"config":611},{"href":612,"dataGaName":558,"dataGaLocation":474},"/de-de/solutions/gitops/",{"text":176,"config":614},{"href":178,"dataGaName":179,"dataGaLocation":474},{"text":181,"config":616},{"href":183,"dataGaName":184,"dataGaLocation":474},{"text":186,"config":618},{"href":188,"dataGaName":189,"dataGaLocation":474},{"text":620,"config":621},"Bildungswesen",{"href":622,"dataGaName":623,"dataGaLocation":474},"/de-de/solutions/education/","education",{"text":625,"config":626},"Finanzdienstleistungen",{"href":627,"dataGaName":628,"dataGaLocation":474},"/de-de/solutions/finance/","financial services",{"title":196,"links":630},[631,633,635,637,640,642,644,646,648,650,652,654],{"text":208,"config":632},{"href":210,"dataGaName":211,"dataGaLocation":474},{"text":213,"config":634},{"href":215,"dataGaName":216,"dataGaLocation":474},{"text":218,"config":636},{"href":220,"dataGaName":221,"dataGaLocation":474},{"text":223,"config":638},{"href":225,"dataGaName":639,"dataGaLocation":474},"docs",{"text":245,"config":641},{"href":247,"dataGaName":248,"dataGaLocation":474},{"text":240,"config":643},{"href":242,"dataGaName":243,"dataGaLocation":474},{"text":250,"config":645},{"href":252,"dataGaName":253,"dataGaLocation":474},{"text":258,"config":647},{"href":260,"dataGaName":261,"dataGaLocation":474},{"text":263,"config":649},{"href":265,"dataGaName":266,"dataGaLocation":474},{"text":268,"config":651},{"href":270,"dataGaName":271,"dataGaLocation":474},{"text":273,"config":653},{"href":275,"dataGaName":276,"dataGaLocation":474},{"text":278,"config":655},{"href":280,"dataGaName":281,"dataGaLocation":474},{"title":296,"links":657},[658,660,662,664,666,668,670,675,680,682,684,686],{"text":303,"config":659},{"href":305,"dataGaName":298,"dataGaLocation":474},{"text":308,"config":661},{"href":310,"dataGaName":311,"dataGaLocation":474},{"text":316,"config":663},{"href":318,"dataGaName":319,"dataGaLocation":474},{"text":321,"config":665},{"href":323,"dataGaName":324,"dataGaLocation":474},{"text":326,"config":667},{"href":328,"dataGaName":329,"dataGaLocation":474},{"text":331,"config":669},{"href":333,"dataGaName":334,"dataGaLocation":474},{"text":671,"config":672},"Nachhaltigkeit",{"href":673,"dataGaName":674,"dataGaLocation":474},"/sustainability/","Sustainability",{"text":676,"config":677},"Vielfalt, Inklusion und Zugehörigkeit",{"href":678,"dataGaName":679,"dataGaLocation":474},"/de-de/diversity-inclusion-belonging/","Diversity, inclusion and belonging",{"text":336,"config":681},{"href":338,"dataGaName":339,"dataGaLocation":474},{"text":346,"config":683},{"href":348,"dataGaName":349,"dataGaLocation":474},{"text":351,"config":685},{"href":353,"dataGaName":354,"dataGaLocation":474},{"text":687,"config":688},"Transparenzerklärung zu moderner Sklaverei",{"href":689,"dataGaName":690,"dataGaLocation":474},"https://handbook.gitlab.com/handbook/legal/modern-slavery-act-transparency-statement/","modern slavery transparency statement",{"items":692},[693,695,698],{"text":522,"config":694},{"href":524,"dataGaName":525,"dataGaLocation":474},{"text":696,"config":697},"Cookies",{"dataGaName":534,"dataGaLocation":474,"id":535,"isOneTrustButton":30},{"text":527,"config":699},{"href":529,"dataGaName":530,"dataGaLocation":474},[701],{"id":702,"title":9,"body":28,"config":703,"content":705,"description":28,"extension":27,"meta":709,"navigation":30,"path":710,"seo":711,"stem":712,"__hash__":713},"blogAuthors/en-us/blog/authors/michael-friedrich.yml",{"template":704},"BlogAuthor",{"name":9,"config":706},{"headshot":707,"ctfId":708},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1749659879/Blog/Author%20Headshots/dnsmichi-headshot.jpg","dnsmichi",{},"/en-us/blog/authors/michael-friedrich",{},"en-us/blog/authors/michael-friedrich","lJ-nfRIhdG49Arfrxdn1Vv4UppwD51BB13S3HwIswt4",[715,728,742],{"content":716,"config":726},{"title":717,"description":718,"authors":719,"heroImage":721,"date":722,"body":723,"category":11,"tags":724},"CI/CD-Observability im Unternehmensmaßstab aufbauen","Dieser Praxisleitfaden zu GitLab Pipeline Analytics hilft Self-Managed-Nutzern, mit Prometheus und Grafana operationale Insights zu gewinnen.",[720],"Paul Meresanu","https://res.cloudinary.com/about-gitlab-com/image/upload/v1774465167/n5hlvrsrheadeccyr1oz.png","2026-04-28","CI/CD-Optimierung beginnt mit Transparenz. Eine erfolgreiche DevOps-Plattform\nim Unternehmensmaßstab umfasst das Verständnis von Pipeline-Performance,\nJob-Ausführungsmustern und quantifizierbaren operationalen Insights – insbesondere\nfür Unternehmen, die GitLab Self-Managed betreiben.\n\nUm GitLab-Kund(inn)en dabei zu helfen, den vollen Nutzen ihrer Plattform\nauszuschöpfen, haben wir die GitLab CI/CD Observability-Lösung als Teil unseres\nPlatform Excellence-Programms entwickelt. Sie verwandelt rohe Pipeline-Metriken\nin handlungsrelevante operationale Erkenntnisse.\n\nEin führendes Finanzdienstleistungsunternehmen hat gemeinsam mit GitLabs Customer\nSuccess Architect Transparenz über seine GitLab Self-Managed-Deployments\ngewonnen. Gemeinsam haben wir eine containerisierte Observability-Lösung\nimplementiert, die den Open-Source-gitlab-ci-pipelines-exporter mit\nunternehmensgerechter Prometheus- und Grafana-Infrastruktur kombiniert.\n\nIn diesem Artikel werden die Herausforderungen beim Pipeline-Management im\nUnternehmensmaßstab erläutert – und wie GitLab CI/CD Observability diese mit\neiner praxisnahen End-to-End-Implementierung adressiert.\n\n\n## Die Herausforderung: CI/CD-Performance messen\n\nVor der Implementierung einer Observability-Lösung sollte die\nMessdimension klar definiert sein:\n\n* **Welche Metriken sind relevant?** Pipeline-Dauer, Job-Erfolgsraten,\n  Queue-Zeiten, Runner-Auslastung\n* **Wer braucht Transparenz?** Entwickler(innen), DevOps-Engineers,\n  Plattformteams, Führungsebene\n* **Welche Entscheidungen werden damit getroffen?** Infrastrukturinvestitionen,\n  Engpass-Behebung, Kapazitätsplanung\n\n\n## Lösungsarchitektur: Ein vollständiges Dashboard-Set für Observability\n\nNach dem Deployment stellt der Observability-Stack ein Set von\nGrafana-Dashboards bereit, das Echtzeit- und historische Transparenz über die\nCI/CD-Plattform bietet. Ein typisches Deployment umfasst:\n\n* **Pipeline Overview Dashboard:** Eine übergeordnete Ansicht mit Gesamtzahl\n  der Pipeline-Läufe, Erfolgs-/Fehlerquoten über die Zeit (als gestapelte\n  Balken- oder Zeitreihencharts) und Trends bei der durchschnittlichen\n  Pipeline-Dauer. Panels verwenden farbcodierte Statusindikatoren (Grün für\n  Erfolg, Rot für Fehler, Gelb für Abbruch), damit Plattformteams\n  Verschlechterungen auf einen Blick erkennen.\n* **Job Performance Dashboard:** Drill-down-Panels mit Verteilungen der\n  einzelnen Job-Dauern (Histogramm), den 10 langsamsten Jobs nach\n  Durchschnittsdauer und Job-Fehler-Heatmaps nach Projekt und Stage. Hier\n  identifizieren Teams konkrete Engpass-Jobs, die sich zu optimieren lohnen.\n* **Runner & Infrastructure Dashboard:** Kombiniert Node-Exporter-Host-Metriken\n  (CPU, Arbeitsspeicher, Disk) mit Pipeline-Queue-Zeit-Daten, um\n  Infrastruktur-Sättigung mit Pipeline-Wartezeiten zu korrelieren. Nützlich\n  für Kapazitätsplanungsentscheidungen wie die Skalierung von Runner-Pools oder\n  das Upgrade von Instanzgrößen.\n* **Deployment Frequency Dashboard:** Verfolgt Deployment-Anzahl und\n  -Dauer über die Zeit pro Umgebung, abgestimmt auf DORA-Metriken. Hilft\n  der Engineering-Führungsebene, Lieferdurchsatz und Environment-Drift\n  (Commits hinter main) zu bewerten.\n\nJedes Dashboard wird automatisch über Grafanas dateibasiertes Provisioning\nbereitgestellt, sodass es konsistent über alle Umgebungen hinweg deployed wird.\nDie Dashboards lassen sich über Grafana-Variablen weiter anpassen, um nach\nProjekt, Ref/Branch oder Zeitraum zu filtern.\n\n![Lösungsarchitektur](https://res.cloudinary.com/about-gitlab-com/image/upload/v1777382608/Blog/Imported/blog-building-ci-cd-observability-stack-for-gitlab-self-managed/image1.png)\n\nDie Lösung benötigt zwei Exporter:\n\n* **Pipeline Exporter:** Erfasst CI/CD-Metriken über die GitLab API\n  (Pipeline-Dauer, Job-Status, Deployments)\n* **Node Exporter:** Erfasst Host-Metriken (CPU, Arbeitsspeicher, Disk)\n  für die Infrastruktur-Korrelation\n\n**Voraussetzungen:**\n\n* GitLab Self-Managed Version 18.1+\n* **Container-Orchestrierungsplattform:** Ein Kubernetes-Cluster (empfohlen\n  für Unternehmens-Deployments) oder eine Container-Runtime wie Docker/Podman\n  für kleinere Umgebungen oder Proof-of-Concept-Deployments. Die primäre\n  Deployment-Anleitung unten zielt auf Kubernetes; eine Docker-Compose-Alternative\n  ist im Anhang für lokales Testen und Evaluation verfügbar\n* GitLab Personal Access Token (Scope **read_api**)\n\nDie vollständige Implementierungsanleitung mit allen Kubernetes-Manifesten\nfolgt direkt im Anschluss.\n\n\n## Kubernetes-Deployment (empfohlen)\n\nFür Unternehmensumgebungen wird jede Komponente als separates Deployment in\neinem dedizierten Namespace deployed. Dieser Ansatz integriert sich in\nbestehende Cluster-Infrastruktur, Secrets-Management und Network-Policies.\n\n### 1. Namespace und Secret erstellen\n\n```bash\nkubectl create namespace gitlab-observability\n\n# GitLab-Token-Secret erstellen (siehe Abschnitt Secrets-Management\n# für unternehmensgerechte Ansätze mit externen Secret-Operatoren)\nkubectl create secret generic gitlab-token \\\n  --from-literal=token=glpat-xxxxxxxxxxxx \\\n  -n gitlab-observability\n```\n\n### 2. Pipeline Exporter deployen\n\n```yaml\n# exporter-deployment.yaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: gitlab-ci-pipelines-exporter\n  namespace: gitlab-observability\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: gitlab-ci-pipelines-exporter\n  template:\n    metadata:\n      labels:\n        app: gitlab-ci-pipelines-exporter\n    spec:\n      containers:\n        - name: exporter\n          image: mvisonneau/gitlab-ci-pipelines-exporter:latest\n          ports:\n            - containerPort: 8080\n          env:\n            - name: GCPE_GITLAB_TOKEN\n              valueFrom:\n                secretKeyRef:\n                  name: gitlab-token\n                  key: token\n            - name: GCPE_CONFIG\n              value: /etc/gcpe/config.yml\n          volumeMounts:\n            - name: config\n              mountPath: /etc/gcpe\n      volumes:\n        - name: config\n          configMap:\n            name: gcpe-config\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: gitlab-ci-pipelines-exporter\n  namespace: gitlab-observability\nspec:\n  selector:\n    app: gitlab-ci-pipelines-exporter\n  ports:\n    - port: 8080\n      targetPort: 8080\n```\n\n### 3. Node Exporter deployen (DaemonSet)\n\n```yaml\n# node-exporter-daemonset.yaml\napiVersion: apps/v1\nkind: DaemonSet\nmetadata:\n  name: node-exporter\n  namespace: gitlab-observability\nspec:\n  selector:\n    matchLabels:\n      app: node-exporter\n  template:\n    metadata:\n      labels:\n        app: node-exporter\n    spec:\n      containers:\n        - name: node-exporter\n          image: prom/node-exporter:latest\n          ports:\n            - containerPort: 9100\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: node-exporter\n  namespace: gitlab-observability\nspec:\n  selector:\n    app: node-exporter\n  ports:\n    - port: 9100\n      targetPort: 9100\n```\n\n### 4. Prometheus deployen\n\n```yaml\n# prometheus-deployment.yaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: prometheus\n  namespace: gitlab-observability\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: prometheus\n  template:\n    metadata:\n      labels:\n        app: prometheus\n    spec:\n      containers:\n        - name: prometheus\n          image: prom/prometheus:latest\n          ports:\n            - containerPort: 9090\n          volumeMounts:\n            - name: config\n              mountPath: /etc/prometheus\n      volumes:\n        - name: config\n          configMap:\n            name: prometheus-config\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: prometheus\n  namespace: gitlab-observability\nspec:\n  selector:\n    app: prometheus\n  ports:\n    - port: 9090\n      targetPort: 9090\n```\n\n### 5. Grafana deployen\n\nDas folgende Grafana-Deployment startet mit deaktivierter Authentifizierung\n(`GF_AUTH_ANONYMOUS_ENABLED: true`) für den einfachen Einstieg.\n\n**Diese Einstellung erlaubt jedem mit Netzwerkzugang, alle Dashboards ohne\nAnmeldung einzusehen.** Für Produktions-Deployments diese Variable entfernen\noder auf false setzen und einen geeigneten Authentifizierungs-Provider\n(LDAP, SAML/SSO oder OAuth) konfigurieren, um den Zugriff auf autorisierte\nNutzende zu beschränken.\n\n```yaml\n# grafana-deployment.yaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: grafana\n  namespace: gitlab-observability\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: grafana\n  template:\n    metadata:\n      labels:\n        app: grafana\n    spec:\n      containers:\n        - name: grafana\n          image: grafana/grafana:10.0.0\n          ports:\n            - containerPort: 3000\n          env:\n            # Für Produktion ENTFERNEN oder auf 'false' setzen.\n            # Bei 'true' können alle Nutzenden mit Netzwerkzugang\n            # Dashboards ohne Authentifizierung einsehen.\n            - name: GF_AUTH_ANONYMOUS_ENABLED\n              value: 'true'\n          volumeMounts:\n            - name: dashboards-provider\n              mountPath: /etc/grafana/provisioning/dashboards\n            - name: datasources\n              mountPath: /etc/grafana/provisioning/datasources\n            - name: dashboards\n              mountPath: /var/lib/grafana/dashboards\n      volumes:\n        - name: dashboards-provider\n          configMap:\n            name: grafana-dashboards-provider\n        - name: datasources\n          configMap:\n            name: grafana-datasources\n        - name: dashboards\n          configMap:\n            name: grafana-dashboards\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: grafana\n  namespace: gitlab-observability\nspec:\n  selector:\n    app: grafana\n  ports:\n    - port: 3000\n      targetPort: 3000\n```\n\n### 6. Network Policy setzen\n\nDen Inter-Pod-Traffic auf die erforderlichen Kommunikationspfade beschränken:\n\n```yaml\n# network-policy.yaml\napiVersion: networking.k8s.io/v1\nkind: NetworkPolicy\nmetadata:\n  name: observability-policy\n  namespace: gitlab-observability\nspec:\n  podSelector: {}\n  policyTypes:\n    - Ingress\n  ingress:\n    # Prometheus scrapt Exporter und Node-Exporter\n    - from:\n        - podSelector:\n            matchLabels:\n              app: prometheus\n      ports:\n        - port: 8080\n        - port: 9100\n    # Grafana fragt Prometheus ab\n    - from:\n        - podSelector:\n            matchLabels:\n              app: grafana\n      ports:\n        - port: 9090\n```\n\n### 7. Validieren\n\n```bash\nkubectl get pods -n gitlab-observability\nkubectl port-forward svc/grafana 3000:3000 -n gitlab-observability\ncurl http://localhost:3000/api/health\n```\n\n\n## Konfigurationsreferenz\n\n### Exporter-Konfiguration\n\n```yaml\n# gitlab-ci-pipelines-exporter.yml (ConfigMap: gcpe-config)\nlog:\n  level: info\ngitlab:\n  url: https://gitlab.your-domain.com\n  maximum_requests_per_second: 10\nproject_defaults:\n  pull:\n    pipeline:\n      jobs:\n        enabled: true\nwildcards:\n  - owner:\n      name: your-group-name\n      kind: group\n    archived: false\n```\n\n### Prometheus-Konfiguration\n\n```yaml\n# prometheus.yml (ConfigMap: prometheus-config)\nglobal:\n  scrape_interval: 15s\nscrape_configs:\n  - job_name: 'gitlab-ci-pipelines-exporter'\n    static_configs:\n      - targets: ['gitlab-ci-pipelines-exporter:8080']\n  - job_name: 'node-exporter'\n    static_configs:\n      - targets: ['node-exporter:9100']\n```\n\n### Grafana-Datenquellen\n\n```yaml\n# datasources.yml (ConfigMap: grafana-datasources)\napiVersion: 1\ndatasources:\n  - name: Prometheus\n    type: prometheus\n    access: proxy\n    url: http://prometheus:9090\n    isDefault: true\n# dashboards.yml (ConfigMap: grafana-dashboards-provider)\napiVersion: 1\nproviders:\n  - name: 'default'\n    folder: 'GitLab CI/CD'\n    type: file\n    options:\n      path: /var/lib/grafana/dashboards\n```\n\n\n## Wichtige Metriken\n\n### Pipeline-Exporter-Metriken\n\n| Metrik | Beschreibung |\n| :---- | :---- |\n| `gitlab_ci_pipeline_duration_seconds` | Pipeline-Ausführungszeit |\n| `gitlab_ci_pipeline_status` | Pipeline-Erfolg/-Fehler nach Projekt |\n| `gitlab_ci_pipeline_job_duration_seconds` | Einzelne Job-Ausführungszeit |\n| `gitlab_ci_pipeline_job_status` | Job-Erfolgs-/-Fehlerstatus |\n| `gitlab_ci_pipeline_job_artifact_size_bytes` | Artifact-Speicherverbrauch |\n| `gitlab_ci_pipeline_coverage` | Code-Coverage-Prozentsatz |\n| `gitlab_ci_environment_deployment_count` | Deployment-Häufigkeit |\n| `gitlab_ci_environment_deployment_duration_seconds` | Deployment-Ausführungszeit |\n| `gitlab_ci_environment_behind_commits_count` | Environment-Drift gegenüber main |\n\n### Node-Exporter-Metriken\n\n| Metrik | Beschreibung |\n| :---- | :---- |\n| `node_cpu_seconds_total` | CPU-Auslastung |\n| `node_memory_MemAvailable_bytes` | Verfügbarer Arbeitsspeicher |\n| `node_filesystem_avail_bytes` | Verfügbarer Festplattenspeicher |\n| `node_load1` | 1-Minuten-Lastdurchschnitt |\n\n\n## Fehlerbehebung\n\n### Grafana-Plugin-Installation in Air-gapped-Umgebungen\n\nFür Offline-Umgebungen Plugins manuell installieren. Beispiel für Kubernetes:\n\n```bash\n# Plugin-ZIP in den Grafana-Pod kopieren\nkubectl cp grafana-polystat-panel-2.1.16.zip \\\n  gitlab-observability/grafana-\u003Cpod-id>:/tmp/\n# Plugin entpacken\nkubectl exec -it -n gitlab-observability deploy/grafana -- \\\n  sh -c \"unzip /tmp/grafana-polystat-panel-2.1.16.zip -d /var/lib/grafana/plugins/\"\n# Grafana-Pod neu starten\nkubectl rollout restart deployment/grafana -n gitlab-observability\n# Installation prüfen\nkubectl exec -it -n gitlab-observability deploy/grafana -- \\\n  ls -al /var/lib/grafana/plugins/\n```\n\n\n## Unternehmensaspekte\n\nFür regulierte Branchen gilt:\n\n* **Token-Sicherheit:** GitLab Personal Access Tokens in einem dedizierten\n  Secrets-Manager speichern, nicht hartcodiert in ConfigMaps. Token-Rotation\n  durchsetzen und den Scope auf **read\\_api** beschränken.\n* **Netzwerksegmentierung:** Hinter einem Reverse Proxy mit TLS-Terminierung\n  deployen. In Kubernetes einen Ingress-Controller mit automatisierter\n  Zertifikatsbereitstellung verwenden.\n* **Authentifizierung:** Grafana mit dem Identity Provider der Organisation\n  konfigurieren (SAML, LDAP oder OAuth/OIDC), um rollenbasierte\n  Zugriffskontrolle auf Dashboards durchzusetzen.\n\n\n## Warum GitLab?\n\nGitLabs API-First-Design ermöglicht individuelle Observability-Lösungen, die\nnative Funktionen wie Value Stream Analytics und DORA-Metriken ergänzen. Die\noffene Architektur erlaubt es Unternehmen, bewährte Open-Source-Werkzeuge –\nwie den gitlab-ci-pipelines-exporter – direkt in bestehende\nUnternehmensinfrastruktur zu integrieren, ohne etablierte Workflows zu\nunterbrechen.\n\nMit wachsender Observability-Reife bieten GitLabs eingebaute\nObservability-Funktionen einen natürlichen nächsten Schritt – tiefere,\nintegrierte Transparenz ohne zusätzliche Werkzeuge. Mehr zu den nativen\nPlattformfunktionen unter\n[GitLab Observability](https://docs.gitlab.com/operations/observability/observability/).\n",[112,725,23],"product",{"featured":14,"template":15,"slug":727},"how-to-build-ci-cd-observability-at-scale",{"content":729,"config":740},{"body":730,"title":731,"description":732,"authors":733,"heroImage":735,"date":736,"category":11,"tags":737},"## Abschnitt 1: Das Modell verstehen\n*Für Engineering-Leads und Entscheidungsträger: Konzept, Anwendungsfälle und Architekturprinzipien. Konfigurationsdetails folgen in Abschnitt 2.*\n\nDie meisten CI/CD-Werkzeuge können einen Build ausführen und ein Deployment anstoßen. Der Unterschied zeigt sich erst dann, wenn die Delivery-Anforderungen komplexer werden: ein Monorepo mit einem Dutzend Services, Microservices über mehrere Repositories verteilt, Deployments in Dutzende von Umgebungen gleichzeitig – oder ein Platform-Team, das organisationsweite Standards durchsetzen will, ohne dabei zum Engpass zu werden.\n\nGitLabs Pipeline-Modell wurde für genau diese Komplexität entwickelt. Parent-Child-Pipelines, DAG-Execution, dynamische Pipeline-Generierung, Multi-Project-Trigger, Merge-Request-Pipelines mit Merged-Results-Verarbeitung und CI/CD Components lösen jeweils eine eigene Klasse von Problemen. Da sich diese Bausteine kombinieren lassen, erschließt das vollständige Modell mehr als nur kürzere Pipeline-Laufzeiten.\n\nDieser Artikel beschreibt die fünf Muster, bei denen das Modell seine Stärken deutlich zeigt – jeweils zugeordnet zu einem konkreten Engineering-Szenario. Konfigurationen und Implementierungsdetails folgen in Abschnitt 2.\n\n### 1. Monorepos: Parent-Child-Pipelines und DAG-Execution\n\n**Das Problem:** Ein Monorepo enthält Frontend, Backend und Dokumentation. Jeder Commit löst einen vollständigen Rebuild aller Komponenten aus – auch wenn sich nur eine README-Datei geändert hat.\n\nGitLab kombiniert zwei sich ergänzende Mechanismen: [Parent-Child-Pipelines](https://docs.gitlab.com/ci/pipelines/downstream_pipelines/#parent-child-pipelines) ermöglichen es einer übergeordneten Pipeline, isolierte Child-Pipelines zu starten. [DAG-Execution via `needs`](https://docs.gitlab.com/ci/yaml/#needs) bricht die starre Stage-Reihenfolge auf und startet Jobs, sobald ihre Abhängigkeiten abgeschlossen sind – nicht erst, wenn alle Jobs einer Stage fertig sind.\n\nEine Parent-Pipeline erkennt, welche Teile des Repos sich geändert haben, und löst ausschließlich die betroffenen Child-Pipelines aus. Jeder Service verwaltet seine eigene Pipeline-Konfiguration; Änderungen in einem Service können keine anderen beeinflussen. Damit bleibt die Komplexität beherrschbar, während das Repository und das Team wachsen.\n\nEinen technischen Aspekt gilt es dabei zu kennen: Wenn mehrere Dateien an einen einzelnen `trigger: include:`-Block übergeben werden, fusioniert GitLab sie zu einer einzigen Child-Pipeline-Konfiguration. Jobs aus diesen Dateien teilen denselben Pipeline-Kontext und können sich gegenseitig per `needs:` referenzieren – das ist die Voraussetzung für die DAG-Optimierung. Werden die Dateien stattdessen auf separate Trigger-Jobs aufgeteilt, entsteht jeweils eine isolierte Pipeline, und dateiübergreifende `needs:`-Referenzen funktionieren nicht.\n\nIn großen Monorepos lassen sich Pipeline-Laufzeiten durch DAG-Execution deutlich reduzieren, da Jobs nicht mehr auf unabhängige Arbeitsschritte in derselben Stage warten.\n\n### 2. Microservices: Cross-Repo-Pipelines über mehrere Projekte\n\n**Das Problem:** Frontend und Backend leben in separaten Repositories. Wenn das Frontend-Team eine Änderung ausliefert, ist nicht erkennbar, ob sie die Backend-Integration beeinträchtigt – und umgekehrt.\n\n[Multi-Project-Pipelines](https://docs.gitlab.com/ci/pipelines/downstream_pipelines/#multi-project-pipelines) ermöglichen es, aus einem Projekt heraus eine Pipeline in einem anderen Projekt auszulösen und auf das Ergebnis zu warten. Das auslösende Projekt sieht die verknüpfte Downstream-Pipeline direkt in seiner eigenen Pipeline-Ansicht.\n\nIn der Praxis erstellt die Frontend-Pipeline ein API-Contract-Artifact und veröffentlicht es, bevor die Backend-Pipeline ausgelöst wird. Das Backend ruft dieses Artifact über die [Jobs API](https://docs.gitlab.com/api/jobs/#download-a-single-artifact-file-from-specific-tag-or-branch) ab und validiert es, bevor weitere Schritte erlaubt sind. Wird eine Breaking Change erkannt, schlägt die Backend-Pipeline fehl – und mit ihr die Frontend-Pipeline. Probleme, die bisher erst in der Produktion sichtbar wurden, werden damit im Pipeline-Prozess abgefangen. Die Abhängigkeit zwischen Services wird sichtbar, nachvollziehbar und aktiv verwaltbar.\n\n![Cross-project pipelines](https://res.cloudinary.com/about-gitlab-com/image/upload/v1775738762/Blog/Imported/hackathon-fake-blog-post-s/image4_h6mfsb.png \"Cross-project pipelines\") *Cross-project pipelines*\n\n### 3. Multi-Tenant/Matrix-Deployments: Dynamische Child-Pipelines\n\n**Das Problem:** Dieselbe Anwendung wird in 15 Kundenumgebungen, drei Cloud-Regionen oder den Stages Dev/Staging/Prod deployed. Manuelle Anpassungen je Umgebung führen zu Konfigurationsdrift. Eine separate Pipeline pro Umgebung ist von Anfang an nicht wartbar.\n\n[Dynamische Child-Pipelines](https://docs.gitlab.com/ci/pipelines/downstream_pipelines/#dynamic-child-pipelines) generieren die Pipeline-Struktur zur Laufzeit. Ein Job führt ein Skript aus, das eine YAML-Datei erzeugt – und diese YAML-Datei wird zur Pipeline für den nächsten Schritt. Die Pipeline-Struktur selbst wird damit zu Daten.\n\nDas Generierungsskript iteriert über eine `ENVIRONMENTS`-Variable, statt jede Umgebung fest zu kodieren. Eine neue Umgebung lässt sich durch Anpassen der Variable hinzufügen – ohne Änderungen an der Pipeline-Konfiguration selbst. Trigger-Jobs erben mit `extends:` eine gemeinsame Template-Konfiguration, sodass `strategy: depend` einmal definiert und nicht für jeden Trigger-Job wiederholt wird. Ein `when: manual`-Gate für das Produktions-Deployment ist direkt in den Pipeline-Graph integriert.\n\nPlatform-Teams nutzen dieses Muster, um Dutzende von Umgebungen zu verwalten, ohne Pipeline-Logik zu duplizieren.\n\n![Dynamic pipeline](https://res.cloudinary.com/about-gitlab-com/image/upload/v1775738765/Blog/Imported/hackathon-fake-blog-post-s/image7_wr0kx2.png \"Dynamic pipeline\")\n\n### 4. MR-First-Delivery: Merge-Request-Pipelines, Merged-Results und Workflow-Routing\n\n**Das Problem:** Die Pipeline läuft bei jedem Push auf jeden Branch. Aufwändige Tests werden auf Feature-Branches ausgeführt, die nie gemergt werden. Gleichzeitig gibt es keine Garantie, dass das Getestete dem entspricht, was nach dem Merge auf `main` tatsächlich landet.\n\nGitLab kombiniert drei ineinandergreifende Mechanismen: [Merge-Request-Pipelines](https://docs.gitlab.com/ci/pipelines/merge_request_pipelines/) laufen ausschließlich dann, wenn ein Merge Request existiert – nicht bei jedem Branch-Push. Allein dadurch entfällt ein erheblicher Anteil unnötiger Compute-Ausführungen. [Merged-Results-Pipelines](https://docs.gitlab.com/ci/pipelines/merged_results_pipelines/) gehen einen Schritt weiter: GitLab erstellt einen temporären Merge-Commit aus dem Branch und dem aktuellen Ziel-Branch und führt die Pipeline dagegen aus. Getestet wird damit das tatsächliche Ergebnis des Merges – nicht der Branch in Isolation. [Workflow-Rules](https://docs.gitlab.com/ci/yaml/workflow/) definieren schließlich, welcher Pipeline-Typ unter welchen Bedingungen ausgeführt wird. Die `$CI_OPEN_MERGE_REQUESTS`-Guard verhindert dabei, dass für einen Branch mit offenem MR doppelte Pipelines ausgelöst werden.\n\nDas Ergebnis ist ein Pipeline-Verhalten, das sich je nach Kontext unterscheidet: Ein Push auf einen Feature-Branch ohne offenen MR führt nur Lint und Unit-Tests aus. Sobald ein MR geöffnet wird, wechseln die Workflow-Rules auf eine MR-Pipeline mit der vollständigen Test-Suite gegen das Merged-Result. Ein Merge auf `main` stellt ein manuelles Produktions-Deployment in die Warteschlange. Der Nightly-Scan läuft einmalig als geplante Pipeline – nicht bei jedem Commit.\n\nMerged-Results-Pipelines fangen dabei die Klasse von Fehlern ab, die erst nach einem Merge sichtbar werden – bevor sie `main` erreichen.\n\n### 5. Governed Pipelines: CI/CD Components\n\n**Das Problem:** Das Platform-Team hat den richtigen Weg für Build, Test und Deploy definiert. Jedes Anwendungsteam pflegt jedoch eine eigene `.gitlab-ci.yml` mit subtilen Abweichungen. Security-Scanning wird übersprungen. Deployment-Standards driften. Audits werden aufwändig.\n\n[CI/CD Components](https://docs.gitlab.com/ci/components/) ermöglichen es Platform-Teams, versionierte, wiederverwendbare Pipeline-Bausteine zu veröffentlichen. Anwendungsteams binden sie mit einer einzigen `include:`-Zeile ein – kein Copy-Paste, kein Drift. Components sind über den [CI/CD Catalog](https://docs.gitlab.com/ci/components/#cicd-catalog) auffindbar, sodass Teams bewährte Bausteine finden und übernehmen können, ohne das Platform-Team direkt einschalten zu müssen.\n\nDrei Zeilen `include:` ersetzen hunderte von duplizierten YAML-Zeilen. Das Platform-Team kann einen Security-Fix in einer neuen Komponentenversion veröffentlichen – Teams steigen auf ihrem eigenen Zeitplan um, oder das Platform-Team fixiert alle auf eine Mindestversion. In beiden Fällen propagiert eine Änderung organisationsweit, statt repo-für-repo angewendet zu werden.\n\nKombiniert mit [Resource Groups](https://docs.gitlab.com/ci/resource_groups/) zur Vermeidung konkurrierender Deployments und [Protected Environments](https://docs.gitlab.com/ci/environments/protected_environments/) für Freigabe-Gates entsteht eine governed Delivery-Plattform, auf der **Compliance der Standard ist, nicht die Ausnahme**. Platform-Teams setzen Vorgaben durch, ohne zum Engpass zu werden.\n\n![Component pipeline (imported jobs)](https://res.cloudinary.com/about-gitlab-com/image/upload/v1775738776/Blog/Imported/hackathon-fake-blog-post-s/image2_pizuxd.png \"Component pipeline (imported jobs)\")\n\n## Das Modell als Ganzes\n\nKeines dieser Muster existiert isoliert. Der Wert von GitLabs Pipeline-Modell liegt in der Kombinierbarkeit seiner Bausteine:\n\n- Ein Monorepo nutzt Parent-Child-Pipelines, und jede Child-Pipeline nutzt DAG-Execution.\n- Eine Microservices-Plattform nutzt Multi-Project-Pipelines, und jedes Projekt nutzt MR-Pipelines mit Merged-Results.\n- Eine governed Plattform nutzt CI/CD Components, um die obigen Muster organisationsweit zu standardisieren.\n\nDie meisten Teams entdecken eines dieser Muster, wenn sie auf ein konkretes Problem stoßen. Teams, die das vollständige Modell verstehen, entwickeln daraus eine Delivery-Infrastruktur, die tatsächlich abbildet, wie ihre Engineering-Organisation arbeitet – und mit ihr wächst.\n\n## Weitere Muster\n\nDas Pipeline-Modell geht über die fünf vorgestellten Muster hinaus:\n\n- [Review Apps mit dynamischen Umgebungen](https://docs.gitlab.com/ci/environments/) erstellen für jeden Feature-Branch eine Live-Vorschau und räumen sie automatisch auf, wenn der MR geschlossen wird.\n- [Caching- und Artifact-Strategien](https://docs.gitlab.com/ci/caching/) sind nach der strukturellen Arbeit häufig der direkteste Weg zur weiteren Laufzeitoptimierung – ohne die Pipeline-Struktur zu verändern.\n- [Geplante und API-ausgelöste Pipelines](https://docs.gitlab.com/ci/pipelines/schedules/) eignen sich für Workloads, die nicht bei jedem Code-Push laufen sollten: Nightly-Security-Scans, Compliance-Reports und Release-Automatisierung lassen sich als geplante oder [API-ausgelöste](https://docs.gitlab.com/ci/triggers/) Pipelines mit `$CI_PIPELINE_SOURCE`-Routing modellieren.\n\n> [GitLab Ultimate kostenlos testen](https://about.gitlab.com/de-de/free-trial/) und Pipeline-Logik ab heute einsetzen.\n\n## Für deutsche Unternehmen: Regulatorischer Kontext\n\nTeams, die Pipeline-Governance nach Muster 5 einführen, adressieren dabei möglicherweise auch Anforderungen, die regulatorische Frameworks an sichere Softwareentwicklungsprozesse stellen.\n\nCI/CD Components mit erzwungenen Security-Gates könnten Anforderungen an sichere Entwicklungsprozesse betreffen – beispielsweise in Bereichen, die Frameworks wie NIS2, ISO 27001 oder BSI IT-Grundschutz an den Software-Entwicklungslebenszyklus adressieren. Protected Environments und Resource Groups betreffen ähnliche Themen im Bereich Änderungskontrolle und Umgebungstrennung, wie sie in Governance-Frameworks typischerweise explizit formuliert sind.\n\nMulti-Project-Pipelines mit API-Contract-Validierung (Muster 2) schaffen Sichtbarkeit über Service-Abhängigkeiten hinweg – ein Aspekt, den Frameworks zur Lieferkettensicherheit adressieren.\n\nMerged-Results-Pipelines (Muster 4) dokumentieren automatisch, dass das tatsächliche Merge-Ergebnis getestet wurde, nicht nur der Feature-Branch in Isolation. Dies könnte Anforderungen an nachvollziehbare Änderungsprozesse betreffen, wie sie in Change-Management-Kontrollen verschiedener Sicherheitsframeworks formuliert sind.\n\nFür konkrete Compliance-Anforderungen im eigenen regulatorischen Umfeld empfiehlt sich Rücksprache mit entsprechender Fachberatung.\n\n## Abschnitt 2: Konfiguration und Implementierung\n\n*Für Entwicklungsteams und DevOps-Praktiker: ausgewählte Konfigurationsbeispiele zu den Mustern 1, 4 und 5. Für vollständige Konfigurationen aller Muster: [englischer Originalartikel](https://about.gitlab.com/blog/5-ways-gitlab-pipeline-logic-solves-real-engineering-problems/).*\n\nDie folgenden Konfigurationen sind illustrativ aufgebaut. Die Skripte verwenden `echo`-Befehle, um das Wesentliche sichtbar zu halten. Für den produktiven Einsatz werden die `echo`-Befehle durch die tatsächlichen Build-, Test- und Deploy-Schritte ersetzt.\n\n### Muster 1: Parent-Child-Pipelines und DAG-Execution\n\nEine Parent-Pipeline erkennt Änderungen und löst nur die betroffenen Child-Pipelines aus:\n\n```yaml # .gitlab-ci.yml stages:\n  - trigger\n\ntrigger-services:\n  stage: trigger\n  trigger:\n    include:\n      - local: '.gitlab/ci/api-service.yml'\n      - local: '.gitlab/ci/web-service.yml'\n      - local: '.gitlab/ci/worker-service.yml'\n    strategy: depend\n```\n\nInnerhalb der Child-Pipeline ermöglicht `needs:` DAG-Execution – der Test startet, sobald der Build abgeschlossen ist, ohne auf andere Jobs in derselben Stage zu warten:\n\n```yaml # .gitlab/ci/api-service.yml stages:\n  - build\n  - test\n\nbuild-api:\n  stage: build\n  script:\n    - echo \"Building API service\"\n\ntest-api:\n  stage: test\n  needs: [build-api]\n  script:\n    - echo \"Running API tests\"\n```\n\n![Local downstream pipelines](https://res.cloudinary.com/about-gitlab-com/image/upload/v1775738759/Blog/Imported/hackathon-fake-blog-post-s/image3_vwj3rz.png \"Local downstream pipelines\")\n\n### Muster 4: MR-First-Delivery\n\nWorkflow-Rules, MR-Pipelines und Merged-Results zusammen ergeben ein kontextabhängiges Pipeline-Verhalten:\n\n```yaml # .gitlab-ci.yml workflow:\n  rules:\n    - if: $CI_PIPELINE_SOURCE == \"merge_request_event\"\n    - if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS\n      when: never\n    - if: $CI_COMMIT_BRANCH\n    - if: $CI_PIPELINE_SOURCE == \"schedule\"\n\nstages:\n  - fast-checks\n  - expensive-tests\n  - deploy\n\nlint-code:\n  stage: fast-checks\n  script:\n    - echo \"Running linter\"\n  rules:\n    - if: $CI_PIPELINE_SOURCE == \"push\"\n    - if: $CI_PIPELINE_SOURCE == \"merge_request_event\"\n    - if: $CI_COMMIT_BRANCH == \"main\"\n\nunit-tests:\n  stage: fast-checks\n  script:\n    - echo \"Running unit tests\"\n  rules:\n    - if: $CI_PIPELINE_SOURCE == \"push\"\n    - if: $CI_PIPELINE_SOURCE == \"merge_request_event\"\n    - if: $CI_COMMIT_BRANCH == \"main\"\n\nintegration-tests:\n  stage: expensive-tests\n  script:\n    - echo \"Running integration tests (15 min)\"\n  rules:\n    - if: $CI_PIPELINE_SOURCE == \"merge_request_event\"\n    - if: $CI_COMMIT_BRANCH == \"main\"\n\ne2e-tests:\n  stage: expensive-tests\n  script:\n    - echo \"Running E2E tests (30 min)\"\n  rules:\n    - if: $CI_PIPELINE_SOURCE == \"merge_request_event\"\n    - if: $CI_COMMIT_BRANCH == \"main\"\n\nnightly-comprehensive-scan:\n  stage: expensive-tests\n  script:\n    - echo \"Running full nightly suite (2 hours)\"\n  rules:\n    - if: $CI_PIPELINE_SOURCE == \"schedule\"\n\ndeploy-production:\n  stage: deploy\n  script:\n    - echo \"Deploying to production\"\n  rules:\n    - if: $CI_COMMIT_BRANCH == \"main\"\n      when: manual\n```\n\n![Conditional pipelines (within a branch with no MR)](https://res.cloudinary.com/about-gitlab-com/image/upload/v1775738768/Blog/Imported/hackathon-fake-blog-post-s/image6_dnfcny.png \"Conditional pipelines (within a branch with no MR)\")\n\n![Conditional pipelines (within an MR)](https://res.cloudinary.com/about-gitlab-com/image/upload/v1775738772/Blog/Imported/hackathon-fake-blog-post-s/image1_wyiafu.png \"Conditional pipelines (within an MR)\")\n\n![Conditional pipelines (on the main branch)](https://res.cloudinary.com/about-gitlab-com/image/upload/v1775738774/Blog/Imported/hackathon-fake-blog-post-s/image5_r6lkfd.png \"Conditional pipelines (on the main branch)\")\n\n### Muster 5: CI/CD Components\n\nEine Komponentendefinition aus einer gemeinsamen Bibliothek:\n\n```yaml # templates/deploy.yml spec:\n  inputs:\n    stage:\n      default: deploy\n    environment:\n      default: production\n--- deploy-job:\n  stage: $[[ inputs.stage ]]\n  script:\n    - echo \"Deploying $APP_NAME to $[[ inputs.environment ]]\"\n    - echo \"Deploy URL: $DEPLOY_URL\"\n  environment:\n    name: $[[ inputs.environment ]]\n```\n\nSo bindet ein Anwendungsteam die Komponenten ein:\n\n```yaml # Application repo: .gitlab-ci.yml variables:\n  APP_NAME: \"my-awesome-app\"\n  DEPLOY_URL: \"https://api.example.com\"\n\ninclude:\n  - component: gitlab.com/my-org/component-library/build@v1.0.6\n  - component: gitlab.com/my-org/component-library/test@v1.0.6\n  - component: gitlab.com/my-org/component-library/deploy@v1.0.6\n    inputs:\n      environment: staging\n\nstages:\n  - build\n  - test\n  - deploy\n```\n\n### Orientierung zu den Mustern 2 und 3\n\n**Muster 2 (Multi-Project-Pipelines):** Das Frontend-Repository publiziert ein API-Contract-Artifact und löst anschließend die Backend-Pipeline aus. Das Backend ruft das Artifact über die GitLab Jobs API ab und validiert es. Der `integration-test`-Job läuft dabei nur dann, wenn er von einer Upstream-Pipeline ausgelöst wurde (`$CI_PIPELINE_SOURCE == \"pipeline\"`), nicht bei einem eigenständigen Push. Die Frontend-Projekt-ID wird als CI/CD-Variable gesetzt, um Hardcoding zu vermeiden. Vollständige Konfigurationen beider Repositories: [englischer Originalartikel](https://about.gitlab.com/blog/5-ways-gitlab-pipeline-logic-solves-real-engineering-problems/#2-microservices-cross-repo-multi-project-pipelines).\n\n**Muster 3 (Dynamische Child-Pipelines):** Ein `generate-config`-Job erzeugt zur Laufzeit environment-spezifische YAML-Dateien. Trigger-Jobs nutzen `extends:` für gemeinsam genutzte Konfiguration und `needs:` für sequenzielle Promotion (dev → staging → prod mit manuellem Gate). Vollständige Konfiguration: [englischer Originalartikel](https://about.gitlab.com/blog/5-ways-gitlab-pipeline-logic-solves-real-engineering-problems/#3-multi-tenant--matrix-deployments-dynamic-child-pipelines).\n\n## Weiterführende Artikel\n\n- [Variable and artifact sharing in GitLab parent-child pipelines](https://about.gitlab.com/blog/variable-and-artifact-sharing-in-gitlab-parent-child-pipelines/)\n- [CI/CD inputs: Secure and preferred method to pass parameters to a pipeline](https://about.gitlab.com/blog/ci-cd-inputs-secure-and-preferred-method-to-pass-parameters-to-a-pipeline/)\n- [Tutorial: How to set up your first GitLab CI/CD component](https://about.gitlab.com/blog/tutorial-how-to-set-up-your-first-gitlab-ci-cd-component/)\n- [How to include file references in your CI/CD components](https://about.gitlab.com/blog/how-to-include-file-references-in-your-ci-cd-components/)\n- [FAQ: GitLab CI/CD Catalog](https://about.gitlab.com/blog/faq-gitlab-ci-cd-catalog/)\n- [Building a GitLab CI/CD pipeline for a monorepo the easy way](https://about.gitlab.com/blog/building-a-gitlab-ci-cd-pipeline-for-a-monorepo-the-easy-way/)\n- [A CI/CD component builder's journey](https://about.gitlab.com/blog/a-ci-component-builders-journey/)\n- [CI/CD Catalog goes GA: No more building pipelines from scratch](https://about.gitlab.com/blog/ci-cd-catalog-goes-ga-no-more-building-pipelines-from-scratch/)","5 GitLab-Pipeline-Muster für komplexe Engineering-Herausforderungen","Wie Parent-Child-Pipelines, DAG-Execution, MR-Pipelines und CI/CD Components komplexe Delivery-Probleme lösen – von Monorepos bis zur governed Plattform.",[734],"Omid Khan","https://res.cloudinary.com/about-gitlab-com/image/upload/v1772721753/frfsm1qfscwrmsyzj1qn.png","2026-04-09",[112,738,23,739],"DevOps platform","features",{"featured":30,"template":15,"slug":741},"5-ways-gitlab-pipeline-logic-solves-real-engineering-problems",{"content":743,"config":752},{"title":744,"description":745,"authors":746,"heroImage":748,"date":749,"body":750,"category":11,"tags":751},"GitLab Container Virtual Registry mit Docker Hardened Images einrichten","Mehrere Registries hinter einem Endpunkt – GitLab Container Virtual Registry mit Docker Hardened Images, Caching und Audit-Trail.",[747],"Tim Rizzi","https://res.cloudinary.com/about-gitlab-com/image/upload/v1772111172/mwhgbjawn62kymfwrhle.png","2026-03-12","Wer im Plattformteam arbeitet, kennt solche Gespräche:\n\n*„Security sagt: Wir müssen gehärtete Base-Images verwenden.\"*\n\n*„Prima – wo trage ich jetzt die Credentials für noch eine weitere Registry ein?\"*\n\n*„Und wie stellen wir sicher, dass alle sie auch wirklich nutzen?\"*\n\nOder diese hier:\n\n*„Warum sind unsere Builds so langsam?\"*\n\n*„Wir pullen dasselbe 500-MB-Image in jedem einzelnen Job neu von Docker Hub.\"*\n\n*„Kann man die nicht irgendwo cachen?\"*\n\nIch arbeite bei GitLab an der [Container Virtual Registry](https://docs.gitlab.com/user/packages/virtual_registry/container/) – einem Pull-Through-Cache, der vor den vorgelagerten Registries sitzt: Docker Hub, dhi.io (Docker Hardened Images), MCR und Quay. Teams erhalten einen einzigen Endpunkt zum Pullen. Images werden beim ersten Abruf gecacht; alle nachfolgenden Pulls kommen aus dem Cache. Das Entwicklungsteam muss nicht wissen, aus welchem Upstream ein bestimmtes Image stammt.\n\nDieser Artikel zeigt die Einrichtung der Container Virtual Registry – mit Docker Hardened Images als konkretem Anwendungsfall, da diese Kombination für Teams mit Sicherheitsanforderungen besonders naheliegt.\n\n## Das Problem: Registry-Wildwuchs im Plattformteam\n\nDie Plattformteams, mit denen ich spreche, verwalten Container-Images über drei bis fünf Registries:\n\n- **Docker Hub** für die meisten Base-Images\n- **dhi.io** für Docker Hardened Images (sicherheitskritische Workloads)\n- **MCR** für .NET- und Azure-Tooling\n- **Quay.io** für das Red-Hat-Ökosystem\n- **Interne Registries** für proprietäre Images\n\nJede davon hat eigene Authentifizierungsmechanismen, unterschiedliche Netzwerklatenz und eine eigene Pfadstruktur für Images.\n\nCI/CD-Konfigurationen füllen sich mit registry-spezifischer Logik. Credential-Management wird zum eigenständigen Projekt. Und jeder Pipeline-Job lädt dieselben Base-Images erneut über das Netz – obwohl sie sich seit Wochen nicht geändert haben.\n\nContainer Virtual Registry konsolidiert das: eine Registry-URL, ein Authentifizierungsfluss über GitLab, gecachte Images aus GitLab-Infrastruktur statt wiederholter Internet-Traversierung.\n\n## Funktionsweise\n\nDas Modell ist geradlinig:\n\n```text\n\nPipeline ruft ab:\n  gitlab.com/virtual_registries/container/1000016/python:3.13\n\nVirtual Registry prüft:\n  1. Im Cache vorhanden? → Direkt zurückgeben\n  2. Nein? → Vom Upstream laden, cachen, zurückgeben\n\n\n```\n\nUpstreams werden in Prioritätsreihenfolge konfiguriert. Bei einem eingehenden Pull-Request durchsucht die Virtual Registry die Upstreams der Reihe nach, bis das Image gefunden wird. Das Ergebnis wird für einen konfigurierbaren Zeitraum gecacht – standardmäßig 24 Stunden.\n\n\n```text\n\n┌─────────────────────────────────────────────────────────┐ │                    CI/CD Pipeline                       │ │                          │                              │ │                          ▼                              │ │   gitlab.com/virtual_registries/container/\u003Cid>/image   │ └─────────────────────────────────────────────────────────┘\n                           │\n                           ▼\n┌─────────────────────────────────────────────────────────┐ │            Container Virtual Registry                   │ │                                                         │ │  Upstream 1: Docker Hub ────────────────┐               │ │  Upstream 2: dhi.io (Hardened) ────────┐│               │ │  Upstream 3: MCR ─────────────────────┐││               │ │  Upstream 4: Quay.io ────────────────┐│││               │ │                                      ││││               │ │                    ┌─────────────────┴┴┴┴──┐            │ │                    │        Cache          │            │ │                    │  (manifests + layers) │            │ │                    └───────────────────────┘            │ └─────────────────────────────────────────────────────────┘\n\n```\n\n## Was das konkret bringt – besonders mit Docker Hardened Images\n\n[Docker Hardened Images](https://docs.docker.com/dhi/) zeichnen sich durch minimale Angriffsfläche, nahezu keine bekannten CVEs, vollständige Software Bills of Materials (SBOMs) und SLSA-Provenance aus. Für Teams, die Base-Images für sicherheitskritische Workloads evaluieren, gehören sie auf die Shortlist.\n\nDer Wechsel zu dhi.io erzeugt jedoch dieselbe operative Reibung wie jede neue Registry:\n\n- **Credential-Verteilung**: Docker-Credentials müssen auf alle Systeme verteilt werden, die Images von dhi.io abrufen.\n- **CI/CD-Anpassungen**: Jede Pipeline muss für die Authentifizierung mit dhi.io aktualisiert werden.\n- **Akzeptanzproblem**: Ohne zentrale Steuerung greifen Teams weiterhin auf reguläre Images zurück.\n- **Fehlende Transparenz**: Ob Teams tatsächlich die gehärteten Varianten nutzen, ist kaum nachvollziehbar.\n\nDie Virtual Registry löst jeden dieser Punkte:\n\n**Einzelne Credential**: Teams authentifizieren sich bei GitLab. Die Virtual Registry übernimmt die Upstream-Authentifizierung. Docker-Credentials werden einmalig auf Registry-Ebene konfiguriert und gelten für alle Pulls.\n\n**Keine per-Team-CI/CD-Änderungen**: Pipelines auf die Virtual Registry zeigen lassen – fertig. Die Upstream-Konfiguration ist zentralisiert.\n\n**Schrittweise Einführung**: Da Images mit ihrem vollständigen Pfad gecacht werden, ist im Cache sichtbar, was tatsächlich abgerufen wird. Wird `library/python:3.11` statt der gehärteten Variante gepullt, ist das erkennbar.\n\n**Audit-Trail**: Der Cache zeigt exakt, welche Images aktiv genutzt werden – nachvollziehbar für Compliance-Zwecke und als Grundlage für das Verständnis der tatsächlichen Infrastruktur-Abhängigkeiten.\n\nWer das Konzept verstanden hat und die Einrichtung zu einem späteren Zeitpunkt in Angriff nimmt: Die wesentlichen Konzepte sind damit abgedeckt. Die technische Konfiguration folgt im nächsten Abschnitt.\n\n## Einrichtung\n\nDie folgende Einrichtung nutzt den Python-Client aus dem Demo-Projekt.\n\n### Virtual Registry erstellen\n\n```python\nfrom virtual_registry_client import VirtualRegistryClient\nclient = VirtualRegistryClient()\nregistry = client.create_virtual_registry(\n    group_id=\"785414\",  # ID der obersten Gruppe\n    name=\"platform-images\",\n    description=\"Cached container images for platform teams\"\n)\nprint(f\"Registry ID: {registry['id']}\") # Diese ID wird für die Pull-URL benötigt\n```\n\n### Docker Hub als Upstream hinzufügen\n\nFür offizielle Images wie Alpine, Python usw.:\n\n```python\n\ndocker_upstream = client.create_upstream(\n    registry_id=registry['id'],\n    url=\"https://registry-1.docker.io\",\n    name=\"Docker Hub\",\n    cache_validity_hours=24\n)\n\n```\n\n### Docker Hardened Images (dhi.io) hinzufügen\n\nDocker Hardened Images werden auf `dhi.io` gehostet – einer separaten Registry mit Authentifizierungspflicht:\n\n```python\n\ndhi_upstream = client.create_upstream(\n    registry_id=registry['id'],\n    url=\"https://dhi.io\",\n    name=\"Docker Hardened Images\",\n    username=\"your-docker-username\",\n    password=\"your-docker-access-token\",\n    cache_validity_hours=24\n)\n\n```\n\n### Weitere Upstreams hinzufügen\n\n```python\n\n# MCR für .NET-Teams client.create_upstream(\n    registry_id=registry['id'],\n    url=\"https://mcr.microsoft.com\",\n    name=\"Microsoft Container Registry\",\n    cache_validity_hours=48\n)\n# Quay für das Red-Hat-Ökosystem client.create_upstream(\n    registry_id=registry['id'],\n    url=\"https://quay.io\",\n    name=\"Quay.io\",\n    cache_validity_hours=24\n)\n\n```\n\n### CI/CD aktualisieren\n\nEine `.gitlab-ci.yml`, die über die Virtual Registry pullt:\n\n```yaml\n\nvariables:\n  VIRTUAL_REGISTRY_ID: \u003Cyour_virtual_registry_ID>\n\n  \nbuild:\n  image: docker:24\n  services:\n    - docker:24-dind\n  before_script:\n    # Authentifizierung bei GitLab – Upstream-Auth wird übernommen\n    - echo \"${CI_JOB_TOKEN}\" | docker login -u gitlab-ci-token --password-stdin gitlab.com\n  script:\n    # Alle Pulls laufen über die zentrale Virtual Registry\n    \n    # Offizielle Docker Hub Images (library/-Präfix erforderlich)\n    - docker pull gitlab.com/virtual_registries/container/${VIRTUAL_REGISTRY_ID}/library/alpine:latest\n    \n    # Docker Hardened Images von dhi.io (kein Präfix nötig)\n    - docker pull gitlab.com/virtual_registries/container/${VIRTUAL_REGISTRY_ID}/python:3.13\n    \n    # .NET von MCR\n    - docker pull gitlab.com/virtual_registries/container/${VIRTUAL_REGISTRY_ID}/dotnet/sdk:8.0\n\n\n```\n\n### Image-Pfadformate\n\nVerschiedene Registries verwenden unterschiedliche Pfadkonventionen:\n\n| Registry | Beispiel-Pull-URL |\n|----------|-------------------|\n| Docker Hub (offiziell) | `.../library/python:3.11-slim` |\n| Docker Hardened Images (dhi.io) | `.../python:3.13` |\n| MCR | `.../dotnet/sdk:8.0` |\n| Quay.io | `.../prometheus/prometheus:latest` |\n\n### Funktionsprüfung\n\nNach einigen Pulls lässt sich der Cache überprüfen:\n\n```python\n\nupstreams = client.list_registry_upstreams(registry['id']) for upstream in upstreams:\n    entries = client.list_cache_entries(upstream['id'])\n    print(f\"{upstream['name']}: {len(entries)} cached entries\")\n\n\n```\n\n## Messergebnisse\n\nTestergebnisse beim Pullen über die Virtual Registry:\n\n| Messgröße | Ohne Cache | Mit warmem Cache |\n|-----------|------------|-----------------|\n| Pull-Zeit (Alpine) | 10,3 s | 4,2 s |\n| Pull-Zeit (Python 3.13 DHI) | 11,6 s | ~4 s |\n| Netzwerk-Roundtrips zum Upstream | Jeder Pull | Nur Cache-Misses |\n\nDer erste Pull hat dieselbe Dauer – das Image muss vom Upstream geladen werden. Jeder weitere Pull innerhalb der Cache-Gültigkeitsdauer kommt direkt aus GitLab-Storage: kein Netzwerk-Hop zu Docker Hub, dhi.io, MCR oder einer anderen Registry.\n\nBei Teams mit vielen Pipeline-Jobs pro Tag summiert sich das zu einem messbaren Gewinn bei den Build-Laufzeiten.\n\n## Praktische Hinweise\n\n### Cache-Gültigkeit\n\nDer Standard sind 24 Stunden. Für sicherheitskritische Images, bei denen Patches schnell verfügbar sein sollen, empfiehlt sich ein kürzeres Intervall:\n\n```python\n\nclient.create_upstream(\n    registry_id=registry['id'],\n    url=\"https://dhi.io\",\n    name=\"Docker Hardened Images\",\n    username=\"your-username\",\n    password=\"your-token\",\n    cache_validity_hours=12\n)\n\n```\n\nFür stabile Images mit fixen Versions-Tags ist ein längeres Intervall problemlos.\n\n### Upstream-Priorität\n\nUpstreams werden der Reihe nach geprüft. Bei gleichnamigen Images in verschiedenen Registries gewinnt der erste passende Upstream.\n\n### Limits\n\n- Maximal 20 Virtual Registries pro Gruppe\n- Maximal 20 Upstreams pro Virtual Registry\n\n## Konfiguration über die Oberfläche\n\nVirtual Registries und Upstreams lassen sich auch direkt in der GitLab-Oberfläche einrichten – ohne API-Aufrufe. Unter **Einstellungen > Pakete und Registries > Virtual Registry** der jeweiligen Gruppe stehen folgende Optionen zur Verfügung:\n\n- Virtual Registries erstellen und verwalten\n- Upstreams hinzufügen, bearbeiten und neu anordnen\n- Cache anzeigen und verwalten\n- Überblick, welche Images abgerufen werden\n\n## Ausblick\n\nIn Entwicklung:\n\n- **Allow/Deny-Listen**: Regex-basierte Steuerung, welche Images aus welchen Upstreams abgerufen werden dürfen.\n\nContainer Virtual Registry befindet sich in der Beta-Phase. Die Funktion wird produktiv eingesetzt und wird weiterentwickelt – Feedback fließt direkt in die Priorisierung ein.\n\n## Feedback\n\nWer als Plattformteam mit Registry-Wildwuchs zu kämpfen hat: Ich möchte verstehen, wie die aktuelle Situation aussieht.\n\n- Wie viele Upstream-Registries werden verwaltet?\n- Wo liegt der größte Schmerzpunkt?\n- Würde ein solcher Ansatz helfen – und falls nicht: Was fehlt?\n\nErfahrungen und Rückmeldungen gerne im [Container Virtual Registry Feedback-Issue](https://gitlab.com/gitlab-org/gitlab/-/work_items/589630) teilen.\n\n## Weiterführende Ressourcen\n\n- [Neue GitLab-Metriken und Registry-Funktionen zur Optimierung von CI/CD-Pipelines](https://about.gitlab.com/de-de/blog/new-gitlab-metrics-and-registry-features-help-reduce-ci-cd-bottlenecks/)\n- [Container Virtual Registry – Dokumentation](https://docs.gitlab.com/user/packages/virtual_registry/container/)\n- [Container Virtual Registry – API](https://docs.gitlab.com/api/container_virtual_registries/)\n\n## Für deutsche Unternehmen könnte dies folgende Themen betreffen\n\nTeams, die sicherheitsgehärtete Base-Images mit vollständigen SBOMs und SLSA-Provenance einsetzen, haben möglicherweise auch Compliance-Überlegungen – beispielsweise in Bereichen wie Sicherheit der Software-Lieferkette, Nachvollziehbarkeit von Image-Abhängigkeiten und zentralem Audit-Trail.\n\nRegulatorische Frameworks wie NIS2 und der Cyber Resilience Act adressieren ähnliche Themen rund um Software-Lieferketten und SBOM-Transparenz. Für konkrete Compliance-Anforderungen empfiehlt sich Rücksprache mit entsprechender Fachberatung.",[23,725,739],{"featured":14,"template":15,"slug":753},"using-gitlab-container-virtual-registry-with-docker-hardened-images",{"promotions":755},[756,770,781,793],{"id":757,"categories":758,"header":760,"text":761,"button":762,"image":767},"ai-modernization",[759],"ai-ml","Is AI achieving its promise at scale?","Quiz will take 5 minutes or less",{"text":763,"config":764},"Get your AI maturity score",{"href":765,"dataGaName":766,"dataGaLocation":248},"/assessments/ai-modernization-assessment/","modernization assessment",{"config":768},{"src":769},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1772138786/qix0m7kwnd8x2fh1zq49.png",{"id":771,"categories":772,"header":773,"text":761,"button":774,"image":778},"devops-modernization",[725,39],"Are you just managing tools or shipping innovation?",{"text":775,"config":776},"Get your DevOps maturity score",{"href":777,"dataGaName":766,"dataGaLocation":248},"/assessments/devops-modernization-assessment/",{"config":779},{"src":780},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1772138785/eg818fmakweyuznttgid.png",{"id":782,"categories":783,"header":785,"text":761,"button":786,"image":790},"security-modernization",[784],"security","Are you trading speed for security?",{"text":787,"config":788},"Get your security maturity score",{"href":789,"dataGaName":766,"dataGaLocation":248},"/assessments/security-modernization-assessment/",{"config":791},{"src":792},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1772138786/p4pbqd9nnjejg5ds6mdk.png",{"id":794,"paths":795,"header":798,"text":799,"button":800,"image":805},"github-azure-migration",[796,797],"migration-from-azure-devops-to-gitlab","integrating-azure-devops-scm-and-gitlab","Is your team ready for GitHub's Azure move?","GitHub is already rebuilding around Azure. Find out what it means for you.",{"text":801,"config":802},"See how GitLab compares to GitHub",{"href":803,"dataGaName":804,"dataGaLocation":248},"/compare/gitlab-vs-github/github-azure-migration/","github azure migration",{"config":806},{"src":780},{"header":808,"blurb":809,"button":810,"secondaryButton":815},"Beginne noch heute, schneller zu entwickeln","Entdecke, was dein Team mit der intelligenten Orchestrierungsplattform für DevSecOps erreichen kann.\n",{"text":811,"config":812},"Kostenlosen Test starten",{"href":813,"dataGaName":54,"dataGaLocation":814},"https://gitlab.com/-/trial_registrations/new?glm_content=default-saas-trial&glm_source=about.gitlab.com/de-de/","feature",{"text":56,"config":816},{"href":58,"dataGaName":59,"dataGaLocation":814},1777493572763]