Code-Synchronisierung: Kundenkopien des Code-Repositorys¶
Jede Plattform betreibt den Code der Plattform aus einem Repository in der Organisation, die die pipelines ausführt: einer Azure-DevOps- Organisation oder einer GitHub-Organisation. Dieses Repository ist eine Kopie (ein Mirror) des Code-Repositorys, das das Produktteam pflegt. Diese Seite beschreibt, wie die Kopie aktuell gehalten wird, wie sie vor lokalen Änderungen geschützt wird und was du prüfen musst, wenn es fehlschlägt. Sie behandelt auch die zweite Art der Synchronisierung im Code: Azure-DevOps-pipelines und GitHub- Workflows identisch halten.
Wer die Synchronisierung ausführt, hängt von deinem Betriebsmodell ab. Im Managed-Modell führt XMMS sie aus, und der Kunde stellt den Zugriff bereit. Im Eigenbetrieb vereinbare mit dem Owner des Codes, wie neue Versionen deine Kopie erreichen. Der Mechanismus unten ist der, den XMMS verwendet.
Warum es das gibt¶
Bevor die Synchronisierung automatisiert war:
- war jedes Update der Kundenkopie ein manueller pull request, und
- konnte die Kundenkopie lokal geändert werden, was mehr als eine Source of Truth erzeugte.
Beides führte zu drift und Überraschungen beim Upgrade. Die automatisierte, einseitige Synchronisierung behebt beides. Sie macht den Code der Kundenkopie identisch mit der Quelle und verhindert zusammen mit der Absicherung unten lokale Änderungen, die die nächste Synchronisierung überschreiben würde.
So funktioniert es¶
flowchart LR
src["Source code repository<br/>(release branches, tags)"] -- "push / tag" --> trig["Trigger"]
trig --> sync["Sync pipeline"]
sync -- "service principal" --> ado["Customer copy<br/>(Azure DevOps)"]
sync -- "GitHub App" --> gh["Customer copy<br/>(GitHub)"]
ado --> cfg["Configuration repository<br/>pins a version"]
gh --> cfg
- Eine Sync-pipeline pusht die Branches und tags des Quell-Repositorys in jede Kundenkopie. Sie kann auch von Hand gestartet werden.
- Der Auslöser ist das Release. Im aktuellen Setup läuft ein GitHub-Workflow im Quell-Repository (
trigger-sync.yaml) bei Pushes aufmain, aufrelease/**und beiv*-tags und übergibt an den Sync-Dispatcher (Releasing). Die erste Version der Synchronisierung war eine Azure-DevOps-pipeline, die bei neuen tags lief, die mitvbeginnen. - Eine Synchronisierung upgradet niemanden. Das Konfigurations-Repository pinnt eine Version (Versionierung), und das Verschieben des pins ist ein bewusster pull request. Die Kopie macht die neue Version nur verfügbar.
Ein Ziel vorbereiten¶
Was du brauchst, hängt davon ab, wo die Kundenkopie liegt. Beide Ziele brauchen jemanden mit Owner-Rechten in der Ziel- Organisation, plane also ein, dass der Kunde diesen Teil übernimmt.
Azure-DevOps-Ziel¶
| Schritt | Wo | Wer |
|---|---|---|
| 1. Den Sync-Service-Principal der Ziel-Azure-DevOps-Organisation hinzufügen | Organisationseinstellungen | Ein Organisations-Owner des Ziels |
| 2. Ihn dem Projekt hinzufügen, das die Kopie enthält, mindestens als Project Reader | Projekteinstellungen | Ein Projektadministrator |
| 3. Ihm die Berechtigungen auf dem Repository geben, die er zum Pushen von Branches und tags braucht | Repository-Sicherheit | Ein Projektadministrator |
| 4. Eine service connection für dieses Ziel in dem Projekt anlegen, das die Sync-pipeline ausführt | Projekteinstellungen des Sync-Projekts | Das Plattform-Team |
Im ersten Setup nutzte der Sync den Produktions-Deployment-Service-Principal der Plattform. Ein dedizierter Service- Principal begrenzt den Schaden, falls sein Secret geleakt wird. Prüfe den Berechtigungssatz gegen das Sync-Template, bevor du dich darauf verlässt.
GitHub-Ziel¶
Die Authentifizierung nutzt eine GitHub App, die in der Ziel-Organisation installiert ist. Ein Owner der GitHub-Organisation führt die folgenden Schritte aus.
-
Erstelle die App in der Organisation (
https://<github host>/organizations/<org>/settings/apps/new):Einstellung Wert Name und Beschreibung Ein Name wie repo-sync, mit einer Beschreibung, die sagt, dass sie die vom Plattform-Team bereitgestellten Repositories synchronisiertWebhook Nicht aktiv Repository-Berechtigungen Contents: read and write. Workflows: read and write Wo sie installiert werden kann Nur in diesem Account -
Notiere die App ID, generiere dann einen private key (
.pem) und lade ihn herunter. - Installiere die App in der Organisation, mit Only select repositories, und wähle nur die Code-Repositories aus, die synchronisiert werden. Notiere die installation ID.
- Speichere im Sync-Projekt den private key als Secret-Variable in der variable group der Sync-pipeline, zum
Beispiel
<customer>_GH_privatekey. Markiere sie als secret.
Die pipeline signiert ein JSON Web Token mit dem private key und der App ID, tauscht es gegen ein Installation Access Token und nutzt dieses Token zum Pushen. Es ist kein Benutzerkonto beteiligt.
Die Kopie absichern¶
Schließe die Kopie, damit niemand sie versehentlich ändert. Eine Änderung in der Kopie würde von der nächsten Synchronisierung überschrieben oder würde die Synchronisierung stoppen.
Azure-DevOps-Ziel. Setze jede Berechtigung jedes anderen Benutzers und jeder anderen Gruppe auf dem Repository auf not set. So kann das Repository gelesen werden, aber ohne manuellen Eingriff ist keine Änderung möglich.
GitHub-Ziel. Deaktiviere GitHub Actions auf dem Repository und schütze alle Branches mit einem ruleset, das nur die App umgehen darf. Beispiel mit der GitHub CLI (PowerShell 7). Fülle die Platzhalter aus und führe es einmal pro Repository aus:
$sync_app_id = "<sync app id>"
$org = "<github organisation>"
$repos = @('<code repository>', '<second code repository, if any>')
gh auth login
$ruleset_name = "RO-enforcement"
foreach ($repo in $repos) {
Write-Host "Configuring $repo"
gh api --method PUT repos/$org/$repo/actions/permissions -F "enabled=false"
$actions_enabled = gh api --method GET repos/$org/$repo/actions/permissions --jq .enabled
Write-Host " - Actions enabled: $actions_enabled. Should be disabled."
$existing_ruleset = gh api /repos/$org/$repo/rulesets --jq ".[] | select(.name == `"$ruleset_name`")"
if (-not $existing_ruleset) {
$body = @"
{
"name": "$ruleset_name",
"target": "branch",
"enforcement": "active",
"conditions": {
"ref_name": { "include": ["~ALL"], "exclude": [] }
},
"rules": [
{"type": "deletion"},
{"type": "non_fast_forward"},
{"type": "creation"},
{"type": "update"}
],
"bypass_actors": [
{ "actor_id": $sync_app_id, "actor_type": "Integration", "bypass_mode": "always" }
]
}
"@
$ruleset_created = $body | gh api --method POST /repos/$org/$repo/rulesets --input -
$ruleset_created = $ruleset_created | ConvertFrom-Json
Write-Host " - Ruleset $($ruleset_created.name) has been created"
}
else {
Write-Host " - Ruleset $ruleset_name already exists"
}
}
Kundenspezifische Änderungen gehören nicht in die Kopie
Eine gesperrte Kopie erzwingt die richtige Gewohnheit: Alle kundenspezifischen Werte gehören in das Konfigurations-Repository, und eine Änderung am Code läuft über das Produktteam. Brauchst du einen Fix vor dem nächsten Release, bitte um ein Patch-Release (Patch-Releases).
Die Sync-pipeline¶
Die pipeline besteht aus zwei Dateien: einer Entry-Datei, die die Ziele auflistet, und einem Template, das den Push ausführt. Das Template läuft über
die Ziele (eine each-Schleife). Die Entry-Datei wird durch tags ausgelöst, die mit v beginnen, und listet einen Eintrag pro Ziel auf:
trigger:
tags:
include:
- v*
pool:
vmImage: "ubuntu-latest"
stages:
- stage: push_remotes
jobs:
- template: sync-template.yaml
parameters:
sourceRepo: ${{ variables.sourceRepo }}
remotes:
- name: "<customer-a>" # Azure DevOps target
remoteUrl: "<host>/<organisation>/<project>/_git/<repository>"
serviceConnection: "<service connection>"
- name: "<customer-b>" # GitHub target
remoteUrl: "<github host>/<organisation>/<repository>"
githubAppPrivateKey: $(<customer-b>_GH_privatekey)
githubAppId: "<app id>"
githubInstallationId: "<installation id>"
Füge ein Ziel hinzu, indem du einen Eintrag und, bei GitHub, die Secret-Variable ergänzt. Für den Plattform-Code und für den älteren spoke-canvas-Code gibt es getrennte Sync-pipelines mit derselben Struktur.
Nach einem Release¶
- Prüfe, dass die Sync-pipeline für jedes Ziel erfolgreich gelaufen ist.
- Öffne ein paar Kundenkopien und prüfe, dass der neue tag existiert und dass die Commit-Historie des Branches aktualisiert ist.
Wenn eine Synchronisierung fehlschlägt¶
| Prüfung | Warum |
|---|---|
| Ist die Credential noch gültig? | Der Sync-Service-Principal hat ein Secret, und die GitHub App hat einen private key. Ein abgelaufenes Secret ist eine häufige Ursache (Secret Watcher) |
| Ist die service connection oder die Secret-Variable für dieses Ziel vorhanden? | Ein neues Ziel braucht sowohl den pipeline-Eintrag als auch die Credentials |
| Hat die Identität noch Zugriff auf die Ziel-Organisation, das Projekt und das Repository? | Jemand könnte den Service Principal entfernt oder Berechtigungen der Absicherung geändert haben |
| Ist die Kundenkopie noch gesperrt und unverändert? | Eine lokale Änderung oder eine Regel, die die Sync-Identität blockiert, stoppt den Push. Behebe es, indem du die Änderung in der Kopie entfernst, nicht indem du die Absicherung für alle abschwächst |
Azure-DevOps- und GitHub-pipelines identisch halten¶
Das Code-Repository enthält zwei pipeline-Systeme, die funktional identisch bleiben müssen: Azure-DevOps-pipelines in
.azure-devops/pipelines/ und GitHub-Actions-Workflows in .github/workflows/. Beide deployen in dieselbe Infrastruktur, daher
verursacht ein Unterschied in der Deployment-Logik, der Layer-Reihenfolge, den Job-Abhängigkeiten oder der Konfigurationsverarbeitung drift und Fehler. Die
Regel: Eine Änderung in einem System wird im selben Commit auch im anderen gemacht.
| Azure DevOps | GitHub Actions |
|---|---|
Entry-pipelines (hub.yaml, hub-destroy.yaml, spoke.yaml, infrastructure.yaml, container.yaml, test.yaml, update-spn.yaml) |
Dieselben Dateinamen in .github/workflows/ |
Stage-Templates in templates/ (stage-mcp-hub.yaml, stage-mcp-spoke.yaml, ...) |
stage-*.yaml Reusable Workflows neben den Entry-Workflows |
Job-Templates in templates/ (job-mcp-hub.yaml, job-mcp-spoke.yaml, job-spn.yaml) |
job-*.yaml Reusable Workflows |
GitHub Actions erlaubt keine Workflow-Dateien in Unterordnern von .github/workflows, daher werden die Templates von Azure DevOps zu
Reusable Workflows, die die Entry-Workflows aufrufen, benannt nach Präfix (stage- und job-).
| Azure DevOps | GitHub Actions | |
|---|---|---|
| Azure-Credentials | Service connections, gesetzt über pipeline-Variablen | OIDC, aufgelöst aus mcp.json |
| Eingaben | parameters (Compile Time ${{ parameters.name }}, Laufzeit $(variable)) |
workflow_call-Inputs (${{ inputs.name }}), Job-Outputs ${{ needs.job.outputs.name }} |
| Struktur | Stages, Jobs, Steps | Jobs und Steps (keine Stages) |
Die Deployment-Reihenfolge, die beide einhalten müssen (siehe auch Deployment-Layer):
- Level 1: foundation (primäre Region), dann connectivity (primäre Region);
- Level 2: foundation, dann connectivity, für die anderen Regionen;
- Level 3: firewall, identity und management parallel, dann postdeployment.
Checkliste für Änderungen:
- [ ] Ermittle, was sich ändert: Layer-Reihenfolge, Job-Logik, Konfigurationsverarbeitung.
- [ ] Mache die Änderung im primären System und übertrage sie dann auf das andere, unter Berücksichtigung der obigen Unterschiede.
- [ ] Aktualisiere die Kommentare in beiden Dateien.
- [ ] Teste beide in einer Nicht-Produktionsumgebung mit
whatIf(nur Plan) und vergleiche die Ausgaben: Sie müssen übereinstimmen. - [ ] Committe beide Änderungen zusammen.
Bist du unsicher, ob eine Änderung ein Gegenstück braucht, frag zuerst. Pipelines, die auseinanderlaufen, verursachen fehlgeschlagene Deployments, drift zwischen Umgebungen und state-Probleme. Ein Skript, das Layer-Reihenfolge und Abhängigkeiten automatisch vergleicht, gibt es noch nicht.