Skip to content

Pipelines and Workflows

Every deployment runs from an entry pipeline in the configuration repository: Azure DevOps YAML under pipelines/, or a GitHub Actions workflow under .github/workflows/. Entry pipelines are thin. They pass a few parameters to a reusable template in the code repository, and that template calls the Xebia.Mcp module. Azure DevOps and GitHub Actions are kept functionally identical (docs/PIPELINE_SYNCHRONIZATION.md and .github/workflows/COUNTERPARTS.md in the code repository).

Entry pipelines

Entry (ADO pipelines/ and GitHub .github/workflows/) Calls template (code repository) Use it for
hub.yaml ADO .azure-devops/pipelines/templates/stage-mcp-hub.yaml, GitHub .github/workflows/stage-mcp-hub.yaml Any hub change: firewall, App Gateway, policies, features, tags, management groups
hub-destroy.yaml same, with destroy: true Tearing down a hub (canary only in the reference repository)
spoke.yaml stage-mcp-spoke.yaml Create or change spokes
spoke-destroy.yaml stage-mcp-spoke.yaml, with destroy: true Delete spokes
infrastructure.yaml stage-infrastructure.yaml Deploy or update the job runners
container.yaml stage-container.yaml Build the mcp container image

The code repository also has:

  • update-spn.yaml: stores and rotates the deployment SPN secret in the hub Key Vault;
  • test.yaml: runner stress test;
  • pull-request.yaml (ADO) and ci.yml (GitHub): CI for the code itself, see Building new functionality;
  • report-version.yaml (Azure DevOps only, scheduled): reports the pinned code version of customer repositories to the platform status API (scripts/version-watcher);
  • trigger-sync.yaml (GitHub only): syncs the code to customer remotes, see Releasing and Code synchronisation.

Which platform environment does each entry target?

Every entry passes one mcp.json per stage or job. Check this before you run, especially for the spoke pipelines: spoke-destroy must use the same mcp.json as the spoke run that created the spoke. In the reference configuration repository, spoke.yaml targets prod while spoke-destroy.yaml targets canary.

Parameters you will change

These are template parameters. In the reference configuration repository most of them are hard-coded in the entry YAML. To change one, edit the entry file in a pull request, or expose it as a run-time parameter (see Expose mode and whatIf at run time).

Parameter Values Effect
mode Plan, Apply, ApproveModifications, ApproveDestroys Plan only plans. Apply plans and applies without stopping. ApproveModifications stops for approval when a plan has any create, update or delete. ApproveDestroys stops only when the plan deletes something.
whatIf true, false true: Terraform still plans, but the apply step is skipped. See the spoke exception below.
approveLayers (hub only) list of layer names, default [] Limits approvals to these layers. Empty means every layer can require approval.
destroy true, false Plans and applies a destroy. Set by the *-destroy entry files.
spokeGlob (spoke only) wildcard pattern, relative to <env-folder>/spokes/ Selects the spoke files, e.g. team-a/canvas_app1_dev.json or team-a/*. Case-insensitive.
environments (spoke only) ordered list of spoke environments Which spoke environments run and in which order. A matched file whose environment is not in the list fails the config test.
imageRef e.g. mcp:latest The container image tag the jobs run in.

Spoke exception for whatIf

The spoke subscription layer is always planned and applied during the Plan step, even with whatIf: true (Invoke-XebiaSpokePlanAction.ps1). Only the spoke canvas layer respects whatIf. Today the subscription layer only creates a subscription alias when azureSubscriptionId is empty.

Check before you run

In the reference configuration repository (xms-cloud-platform-config), hub.yaml uses mode: Apply with whatIf: false, so canary and prod apply without approval. spoke.yaml uses mode: Apply with whatIf: true, so it only plans the spoke canvas. Decide which behavior you want and change the entry files before you rely on them.

What a hub run does

  1. Prepare. Runs the unit tests and the config tests (Pester):

    • firewall JSON against src/Xebia.Mcp/Files/json/firewallschema.json, IP group references and priorities;
    • App Gateway and WAF JSON against their schemas;
    • the DNS baseline.

    It also builds the region matrix. A failing config test stops the run before Terraform starts. 2. For each layer, in layer order: 1. <layer>_plan: terraform plan. The plan file and a JSON summary are published as artifacts <layer>_<region>_<level> and <layer>_<region>_<level>_json. The summary appears as a warning: Plan: X to add, Y to change, Z to destroy. 2. <layer>_analyse and <layer>_approve: only in the Approve* modes. They pause for approval when the plan matches the mode. 3. <layer>_apply: applies the saved plan file, so what you approved is exactly what is applied. Skipped in Plan mode. 3. Clean: releases the runner reservation.

A plan that deletes resources marks the plan task as Succeeded with issues (orange). Treat that as a signal to read the plan.

What a spoke run does

To prepare the identity, service connections, code access and integrations for spoke deployments, see Prepare the spoke pipeline and Prepare spoke integrations.

  1. Prepare: runs the unit tests and the hub config tests (so a broken firewall JSON also blocks spoke runs), then the spoke config tests. It finds *.json under <env-folder>/spokes/ that match spokeGlob and groups them by spoke environment. The run fails if:
    • the glob matches no file;
    • a matched file is not valid against spokeschema.json;
    • a matched file's environment is not in environments;
    • a matched file's managementGroupId is not an id in <env-folder>/<targetRegions.primary>/foundation.auto.tfvars.
  2. Per spoke environment, in the order of environments, and only after the previous one succeeded or was skipped:
    1. Plan: plans and applies the subscription layer, then plans the spoke canvas.
    2. Analyse and Approve: Approve* modes only.
    3. Apply: applies the spoke canvas plan.
  3. Clean.

What the apply creates is described in Spoke canvas. To see afterwards whether a spoke still matches its file, see Configuration drift detection.

Approval gates

How the decision is made:

  • Hub: per layer, the analyse step reads the plan summaries of the regions of that layer and asks for approval when a plan matches the mode (Invoke-XebiaHubAnalyseAction.ps1). Check the Known issues: in the version you pin, the analyse step may only read the last region's summary.
  • Spoke: one analyse step per spoke-environment stage. It reads every plan summary downloaded in that run, including subscription-layer plans and earlier environments. Approval is therefore per environment stage, not per spoke.

Where you approve:

  • Azure DevOps: the <layer>_approve job is a ManualValidation@1 server job. The template sets no approvers and no notifyUsers, so nobody is notified. Watch the run.
  • GitHub Actions: the approval job uses the GitHub environment approval-required. Create this environment in the configuration repository and add required reviewers. Without reviewers the job continues without waiting.

Expose mode and whatIf at run time

Azure DevOps (pipelines/hub.yaml):

parameters:
  - name: mode
    type: string
    default: Plan
    values: [Plan, Apply, ApproveModifications, ApproveDestroys]
  - name: whatIf
    type: boolean
    default: false

stages:
  - template: .azure-devops/pipelines/templates/stage-mcp-hub.yaml@mcp-code
    parameters:
      # ...existing parameters...
      mode: ${{ parameters.mode }}
      whatIf: ${{ parameters.whatIf }}

GitHub Actions (.github/workflows/hub.yaml):

on:
  workflow_dispatch:
    inputs:
      mode:
        type: choice
        options: [Plan, Apply, ApproveModifications, ApproveDestroys]
        default: Plan

jobs:
  canary:
    uses: xmms/xms-cloud-platform-code/.github/workflows/stage-mcp-hub.yaml@release/v4.12
    with:
      # ...existing inputs...
      mode: ${{ inputs.mode }}

Do the same on both platforms to keep parity.

Running a pipeline

Azure DevOps: Pipelines → the entry pipeline (for example hub) → Run pipeline → pick the branch → fill in the parameters (for example spokeGlob) → Run. Open a plan job to read the Terraform plan, or download the plan artifact.

GitHub Actions: Actions → the workflow (for example spoke) → Run workflow → pick the branch → fill in the inputs → Run workflow. Plan output is in the job logs and the uploaded artifacts.

Prerequisites for the pipelines

Azure DevOps

  • Service connections with the names in pipelines/variables.yaml (for example <env>.serviceConnectionNameHub and <env>.serviceConnectionNameSpoke).
  • A GitHub Enterprise service connection for the mcp-code repository resource (endpoint: in each entry file; in the reference repository xebia-managed-services-eu.ghe.com).
  • The agent pools <AgentPool> (Managed DevOps Pool) and <AgentPool>-CTR (container jobs), created by the infrastructure pipeline.

GitHub Actions

  • A federated credential (OIDC) on the service principals in mcp.json (Azure.TerraformState.Hub.ServicePrincipalId and .Spoke.ServicePrincipalId).
  • Repository variable cloud_platform_gh_app_id and secret cloud_platform_gh_app_private_key: the GitHub App that checks out the code repository (.github/actions/checkout-mcp-code).
  • The environment approval-required.

Bootstrapping a new platform environment (order)

  1. Create the Terraform state storage account(s), the spoke state container and the deployment service principal(s), and grant them access. These are prerequisites, not created by the pipelines.
  2. Create <env-folder>/mcp.json, global.auto.tfvars and the region tfvars.
  3. Run infrastructure: job runners, plus the blob container in the hub state storage account.
  4. Run container: the mcp image.
  5. Run hub (first in Plan mode, then Apply).
  6. Run spoke for each spoke.