Skip to content

Module-Based Pipeline Abstraction for Multi-Platform Portability

Context and Problem Statement

The XMS Landing Zone supports multiple customers and may need to run on different CI/CD platforms (Azure DevOps, GitHub Actions, GitLab CI). Platform-specific pipeline YAML files with embedded business logic create several problems:

  • Duplicated logic across different platform definitions
  • Complex YAML that is difficult to test and debug
  • Vendor lock-in to specific pipeline syntax
  • Inability to run deployments locally or test logic outside CI/CD
  • Changes require modifying multiple pipeline files to maintain parity
  • Error handling and logging logic is scattered across YAML definitions

How can we maintain deployment consistency across multiple automation platforms while keeping pipelines simple and maintainable?

Decision Drivers

  • Need to support multiple customers with different CI/CD platform preferences
  • Desire to test deployment logic locally without running full pipelines
  • Requirement for unit testing of deployment orchestration
  • Goal to reduce pipeline complexity and maintenance burden
  • Need for consistent behavior across development, CI, and production environments
  • Desire to version deployment logic independently from pipeline infrastructure
  • Need to handle platform-specific operations (artifact uploads, logging) without duplicating core logic

Considered Options

  1. Module-based abstraction (thin pipelines + PowerShell module)
  2. Pipeline templates with shared YAML
  3. Separate implementations per platform
  4. Container-based CLI tool

Decision Outcome

Chosen option: "Module-based abstraction", because it provides the best balance of portability, testability, and maintainability.

All deployment logic, configuration parsing, Terraform orchestration, and validation is implemented in the Xebia.Mcp PowerShell module at src/Xebia.Mcp/. Pipeline YAML files are thin wrappers that only handle:

  • Triggers and scheduling
  • Authentication and credential setup
  • Calling PowerShell functions with parameters
  • Publishing artifacts and test results
  • Matrix strategies for parallel execution

Implementation Pattern

Pipeline YAML (thin orchestrator):

# Azure DevOps
- task: PowerShell@2
  displayName: 'Create release plan'
  inputs:
    pwsh: true
    targetType: inline
    script: |
      Import-Module ./src/Xebia.Mcp/Xebia.Mcp.psd1
      $config = Resolve-XebiaMcpConfiguration -Environment 'prod'
      Invoke-XebiaMcpReleasePlan -Config $config -Region 'neu' -DeployName 'foundation' -Level '1'

# GitHub Actions
- name: Create release plan
  shell: pwsh
  run: |
    Import-Module ./src/Xebia.Mcp/Xebia.Mcp.psd1
    $config = Resolve-XebiaMcpConfiguration -Environment 'prod'
    Invoke-XebiaMcpReleasePlan -Config $config -Region 'neu' -DeployName 'foundation' -Level '1'

PowerShell module contains all logic:

  • Parallelism decisions (firewall=1, postdeployment=10, others=100)
  • Terraform state backend configuration
  • Plan file creation and JSON summary generation
  • Artifact upload commands
  • Warning detection and formatting
  • Deployment matrix calculation
  • Configuration validation

Module Organization

Functions are organized by domain in src/Xebia.Mcp/Functions/Public/:

Folder Purpose Example Functions
Config/ Configuration file access and validation Get-XebiaEnvironmentItem, Test-XebiaJsonSchema
Runner/ Pipeline-facing operations (may contain platform-specific commands) Invoke-XebiaMcpReleasePlan, Invoke-XebiaMcpReleaseApply
Terraform/ Terraform operations Invoke-XebiaTerraformPlan, Initialize-TerraformWorkspace
Pester/ Test execution Invoke-XebiaPesterTests, Format-PesterResults
Logging/ Logging and output Write-XebiaLog, Format-DeploymentSummary

Runner Functions are the only functions that may contain platform-specific commands (e.g., ##vso[artifact.upload] for Azure DevOps). All other functions are platform-agnostic.

Consequences

  • Good, because deployment logic can be tested with Pester unit tests
  • Good, because developers can run deployments locally using the same functions as CI/CD
  • Good, because migrating between platforms requires minimal effort (only YAML changes)
  • Good, because business logic lives in one place, reducing duplication
  • Good, because PowerShell functions can be versioned and released independently
  • Good, because error handling and validation logic is consistent across all platforms
  • Good, because debugging is easier (step through PowerShell locally vs. pipeline reruns)
  • Bad, because developers must learn PowerShell in addition to Terraform
  • Bad, because Runner functions may contain platform-specific commands requiring conditional logic
  • Neutral, because PowerShell is available on all major CI/CD platforms (Azure DevOps, GitHub Actions, GitLab CI)

Validation

Compliance is validated through:

  1. Code review: New pipeline logic should be rejected if not extracted to module
  2. Test coverage: All public functions must have corresponding .Unit.Tests.ps1 files
  3. Pipeline complexity metrics: YAML files should remain under 200 lines
  4. Equivalence testing: Azure DevOps and GitHub Actions pipelines must call identical functions
  5. Standards enforcement: docs/coding-standards/pipeline-architecture.md provides guidance for all AI tools

Implementation Examples

Example 1: Release Planning

Before (embedded in pipeline):

- script: |
    PARALLELISM=100
    if [ "$DEPLOY_NAME" = "firewall" ]; then
      PARALLELISM=1
    elif [ "$DEPLOY_NAME" = "postdeployment" ]; then
      PARALLELISM=10
    fi

    terraform -chdir=infra/deploy/$DEPLOY_NAME plan \
      -parallelism=$PARALLELISM \
      -out=plan.tfplan \
      -var-file=config/environments/$ENVIRONMENT/$REGION/global.auto.tfvars

    terraform show -json plan.tfplan > plan.json

    CREATES=$(jq '.resource_changes | map(select(.change.actions | contains(["create"]))) | length' plan.json)
    DESTROYS=$(jq '.resource_changes | map(select(.change.actions | contains(["delete"]))) | length' plan.json)

    echo "##vso[task.logissue type=warning]Plan: $CREATES to add, $DESTROYS to destroy"
  displayName: 'Create Terraform plan'

After (module-based):

- task: PowerShell@2
  displayName: 'Create release plan'
  inputs:
    pwsh: true
    targetType: inline
    script: |
      Import-Module ./src/Xebia.Mcp/Xebia.Mcp.psd1
      $config = Resolve-XebiaMcpConfiguration -Environment '${{ parameters.environment }}'
      Invoke-XebiaMcpReleasePlan -Config $config -Region '${{ parameters.region }}' -DeployName '${{ parameters.deployName }}' -Level '${{ parameters.level }}'

All the parallelism logic, state backend configuration, plan parsing, and warning emission is now in Invoke-XebiaMcpReleasePlan where it can be tested and reused.

Example 2: Configuration Validation

Before (embedded in pipeline):

- script: |
    for file in config/environments/$ENVIRONMENT/**/*.json; do
      SCHEMA=$(echo $file | sed 's/.*\///' | sed 's/-.*//')
      if ! test-json -Schema schemas/${SCHEMA}.schema.json -Path $file; then
        echo "##vso[task.logissue type=error]Invalid JSON: $file"
        exit 1
      fi
    done
  displayName: 'Validate configuration files'

After (module-based):

- task: PowerShell@2
  displayName: 'Validate configuration'
  inputs:
    pwsh: true
    targetType: inline
    script: |
      Import-Module ./src/Xebia.Mcp/Xebia.Mcp.psd1
      Test-XebiaMcpConfig -Environment '${{ parameters.environment }}'

The validation logic, schema discovery, error formatting, and reporting is in Test-XebiaMcpConfig and tested with Pester.

More Information

Documentation:

  • docs/coding-standards/pipeline-architecture.md - Detailed guidance for implementing this pattern
  • docs/coding-standards/powershell-functions.md - Function structure and standards
  • docs/coding-standards/powershell-unit-tests.md - Test coverage requirements
  • CONTRIBUTING.md - Quick start guide for contributors

Code examples:

  • src/Xebia.Mcp/Functions/Public/Runner/Invoke-XebiaMcpReleasePlan.ps1 - Pipeline-facing function with platform-specific commands
  • src/Xebia.Mcp/Functions/Public/Terraform/Invoke-XebiaTerraformPlan.ps1 - Platform-agnostic core logic
  • src/Xebia.Mcp/Functions/Public/Config/Test-XebiaJsonSchema.ps1 - Configuration validation logic

Testing:

  • src/Xebia.Mcp/Functions/Public/**/*.Unit.Tests.ps1 - Unit test examples
  • src/Xebia.Mcp/Tests/Smoke/ - Smoke test examples

This decision was formalized on 2026-05-21 based on existing implementation patterns already in use. The ADR documents the rationale and serves as guidance for future development.