Zum Inhalt

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:

  1. war jedes Update der Kundenkopie ein manueller pull request, und
  2. 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 auf main, auf release/** und bei v*-tags und übergibt an den Sync-Dispatcher (Releasing). Die erste Version der Synchronisierung war eine Azure-DevOps-pipeline, die bei neuen tags lief, die mit v beginnen.
  • 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.

  1. 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 synchronisiert
    Webhook Nicht aktiv
    Repository-Berechtigungen Contents: read and write. Workflows: read and write
    Wo sie installiert werden kann Nur in diesem Account
  2. Notiere die App ID, generiere dann einen private key (.pem) und lade ihn herunter.

  3. 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.
  4. 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

  1. Prüfe, dass die Sync-pipeline für jedes Ziel erfolgreich gelaufen ist.
  2. Ö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):

  1. Level 1: foundation (primäre Region), dann connectivity (primäre Region);
  2. Level 2: foundation, dann connectivity, für die anderen Regionen;
  3. 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.