Codesynchronisatie: klantkopieën van de coderepository¶
Elk platform draait de code van het platform vanuit een repository in de organisatie die de pipelines draait: een Azure DevOps- organisatie of een GitHub-organisatie. Die repository is een kopie (een mirror) van de coderepository die het productteam onderhoudt. Deze pagina beschrijft hoe de kopie up-to-date wordt gehouden, hoe ze tegen lokale wijzigingen wordt beschermd en wat je moet controleren als het misgaat. Ze behandelt ook de tweede vorm van synchronisatie in de code: Azure DevOps-pipelines en GitHub- workflows identiek houden.
Wie de synchronisatie draait, hangt af van je beheermodel. In het managed model draait XMMS die en levert de klant de toegang. In het zelfbeheermodel spreek je met de eigenaar van de code af hoe nieuwe versies je kopie bereiken. Het onderstaande mechanisme is dat van XMMS.
Waarom het bestaat¶
Voordat de synchronisatie werd geautomatiseerd:
- was elke update van de klantkopie een handmatige pull request, en
- kon de klantkopie lokaal worden gewijzigd, waardoor er meer dan één bron van waarheid ontstond.
Beide veroorzaakten drift en verrassingen bij het upgraden. De geautomatiseerde, eenrichtingssynchronisatie lost beide op. Ze maakt de code van de klantkopie identiek aan de bron en voorkomt, samen met de onderstaande afscherming, lokale wijzigingen die de volgende synchronisatie zou overschrijven.
Hoe het werkt¶
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
- Een sync pipeline pusht de branches en tags van de bronrepository naar elke klantkopie. Ze kan ook met de hand worden gestart.
- De trigger is de release. In de huidige opzet draait een GitHub-workflow in de bronrepository (
trigger-sync.yaml) bij pushes naarmain, naarrelease/**en bijv*tags, en draagt over aan de sync dispatcher (Releasen). De eerste versie van de synchronisatie was een Azure DevOps-pipeline die draaide bij nieuwe tags die metvbeginnen. - Een synchronisatie upgradet niemand. De configuratierepository pint een versie (Versionering), en het verzetten van de pin is een bewuste pull request. De kopie maakt de nieuwe versie alleen beschikbaar.
Een doel voorbereiden¶
Wat je nodig hebt, hangt af van waar de klantkopie staat. Voor beide doelen is iemand met owner-rechten in de doel- organisatie nodig, dus plan in dat de klant dit deel doet.
Azure DevOps-doel¶
| Stap | Waar | Wie |
|---|---|---|
| 1. Voeg de sync service principal toe aan de Azure DevOps-doelorganisatie | Organisatie-instellingen | Een organisation owner van het doel |
| 2. Voeg hem toe aan het project dat de kopie bevat, minimaal als project reader | Projectinstellingen | Een project administrator |
| 3. Geef hem de rechten op de repository die hij nodig heeft om branches en tags te pushen | Repository-beveiliging | Een project administrator |
| 4. Maak een service connection voor dit doel in het project dat de sync pipeline draait | Projectinstellingen van het sync-project | Het platformteam |
In de eerste opzet gebruikte de sync de service principal voor productiedeployments van het platform. Een eigen service principal beperkt de schade als zijn secret lekt. Controleer de set rechten aan de hand van de sync-template voordat je erop vertrouwt.
GitHub-doel¶
Authenticatie gebruikt een GitHub App die in de doelorganisatie is geïnstalleerd. Een owner van de GitHub-organisatie voert de onderstaande stappen uit.
-
Maak de app aan in de organisatie (
https://<github host>/organizations/<org>/settings/apps/new):Instelling Waarde Naam en beschrijving Een naam zoals repo-sync, met een beschrijving die zegt dat de app de repositories synchroniseert die het platformteam levertWebhook Niet actief Repositorypermissies Contents: read and write. Workflows: read and write Waar hij geïnstalleerd mag worden Alleen op dit account -
Noteer de App ID, genereer en download daarna een private key (
.pem). - Installeer de app in de organisatie, met Only select repositories, en selecteer alleen de coderepositories die worden gesynchroniseerd. Noteer de installation ID.
- Sla in het sync-project de private key op als secret variable in de variable group van de sync pipeline, bijvoorbeeld
<customer>_GH_privatekey. Markeer hem als secret.
De pipeline ondertekent een JSON Web Token met de private key en de App ID, wisselt die in voor een installation access token en gebruikt dat token om te pushen. Er is geen gebruikersaccount bij betrokken.
De kopie afschermen¶
Sluit de kopie af zodat niemand haar per ongeluk wijzigt. Een wijziging in de kopie zou door de volgende synchronisatie worden overschreven, of zou de synchronisatie stoppen.
Azure DevOps-doel. Zet elke permissie van elke andere gebruiker en groep op de repository op not set. Zo kan de repository worden gelezen, maar is geen wijziging mogelijk zonder handmatige ingreep.
GitHub-doel. Schakel GitHub Actions op de repository uit en beveilig alle branches met een ruleset die alleen de app mag omzeilen. Voorbeeld met de GitHub CLI (PowerShell 7). Vul de placeholders in en draai het één keer per repository:
$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"
}
}
Klantspecifieke wijzigingen horen niet in de kopie
Een afgeschermde kopie dwingt de juiste gewoonte af: alle klantspecifieke waarden horen in de configuratierepository, en een wijziging aan de code loopt via het productteam. Heb je een fix nodig vóór de volgende release, vraag dan om een patch release (Patch releases).
De sync pipeline¶
De pipeline heeft twee bestanden: een entry-bestand dat de doelen opsomt en een template die de push doet. De template loopt over
de doelen (een each-loop). Het entry-bestand triggert op tags die met v beginnen en noemt één entry per doel:
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>"
Voeg een doel toe door een entry toe te voegen en, voor GitHub, de secret variable. Er bestaan aparte sync pipelines voor de platformcode en voor de oudere spoke canvas-code, met dezelfde structuur.
Na een release¶
- Controleer dat de sync pipeline voor elk doel succesvol heeft gedraaid.
- Open een paar klantkopieën en controleer dat de nieuwe tag bestaat en dat de commitgeschiedenis van de branch is bijgewerkt.
Als een synchronisatie faalt¶
| Controle | Waarom |
|---|---|
| Is de credential nog geldig? | De sync service principal heeft een secret en de GitHub App heeft een private key. Een verlopen secret is een veelvoorkomende oorzaak (Secret watcher) |
| Is de service connection of de secret variable voor dit doel aanwezig? | Een nieuw doel heeft zowel de pipeline-entry als de credentials nodig |
| Heeft de identiteit nog toegang tot de doelorganisatie, het project en de repository? | Iemand kan de service principal hebben verwijderd of permissies van de afscherming hebben gewijzigd |
| Is de klantkopie nog steeds afgeschermd en ongewijzigd? | Een lokale wijziging, of een regel die de sync-identiteit blokkeert, stopt de push. Los het op door de wijziging in de kopie te verwijderen, niet door de afscherming voor iedereen te verzwakken |
Azure DevOps- en GitHub-pipelines identiek houden¶
De coderepository bevat twee pipelinesystemen die functioneel identiek moeten blijven: Azure DevOps-pipelines in
.azure-devops/pipelines/ en GitHub Actions-workflows in .github/workflows/. Beide deployen naar dezelfde infrastructuur, dus een
verschil in deploymentlogica, layervolgorde, jobafhankelijkheden of configuratieafhandeling veroorzaakt drift en fouten. De
regel: een wijziging in het ene systeem wordt in dezelfde commit in het andere gemaakt.
| Azure DevOps | GitHub Actions |
|---|---|
Entry pipelines (hub.yaml, hub-destroy.yaml, spoke.yaml, infrastructure.yaml, container.yaml, test.yaml, update-spn.yaml) |
Dezelfde bestandsnamen in .github/workflows/ |
Stage-templates in templates/ (stage-mcp-hub.yaml, stage-mcp-spoke.yaml, ...) |
Reusable workflows stage-*.yaml naast de entry-workflows |
Job-templates in templates/ (job-mcp-hub.yaml, job-mcp-spoke.yaml, job-spn.yaml) |
Reusable workflows job-*.yaml |
GitHub Actions staat geen workflowbestanden toe in submappen van .github/workflows, dus de templates van Azure DevOps worden
reusable workflows die de entry-workflows aanroepen, benoemd met een prefix (stage- en job-).
| Azure DevOps | GitHub Actions | |
|---|---|---|
| Azure-credentials | Service connections, ingesteld via pipelinevariabelen | OIDC, opgelost vanuit mcp.json |
| Inputs | parameters (compile time ${{ parameters.name }}, run time $(variable)) |
workflow_call-inputs (${{ inputs.name }}), joboutputs ${{ needs.job.outputs.name }} |
| Structuur | Stages, jobs, steps | Jobs en steps (geen stages) |
De deploymentvolgorde die beide moeten behouden (zie ook Deployment layers):
- level 1: foundation (primaire regio), daarna connectivity (primaire regio);
- level 2: foundation, daarna connectivity, voor de andere regio's;
- level 3: firewall, identity en management parallel, daarna postdeployment.
Checklist voor wijzigingen:
- [ ] Bepaal wat er verandert: layervolgorde, joblogica, configuratieafhandeling.
- [ ] Maak de wijziging in het primaire systeem en vertaal haar daarna naar het andere, rekening houdend met de bovenstaande verschillen.
- [ ] Werk de opmerkingen in beide bestanden bij.
- [ ] Test beide in een niet-productieomgeving, met
whatIf(alleen plan), en vergelijk de uitvoer: die moet overeenkomen. - [ ] Commit beide wijzigingen samen.
Weet je niet zeker of een wijziging een tegenhanger nodig heeft, vraag dan eerst. Pipelines die uit elkaar lopen veroorzaken mislukte deployments, drift tussen omgevingen en state-problemen. Een script dat layervolgorde en afhankelijkheden automatisch vergelijkt bestaat nog niet.