Pipeline B: The "Assembly Line" (Your Main Pipeline)

27 October 2025
Previous Post
Next Post

This is your main application pipeline, optimized for speed by leveraging the pre-built Docker image from the "Factory" pipeline. It runs frequently (on code pushes and PRs) and handles testing and deployment.

1. The Orchestrator (azure-pipelines.yml)

This file is the main entry point for your application's CI/CD process. It defines the stages, triggers, and crucially, the logic for selecting the correct deployment environment based on the build trigger.

Key Features:

  • Conditional Environment Selection: The environment_azure variable uses compile-time expressions (${{ ... }}) to dynamically choose the target Azure DevOps Environment.
    • Pushes to main target [PROJECT_NAME]-app-prod (which should have manual approvals).
    • Pushes/Merges to develop (Build.Reason == 'IndividualCI') target [PROJECT_NAME]-app-dev-ci (which must be configured without approvals for automatic deployment).
    • Manual runs targeting develop (or other non-main branches) target [PROJECT_NAME]-app-dev (which should have manual approvals).
  • imageBuilderPipelineName: This variable holds the exact name of your "Factory" pipeline as it appears in the Azure DevOps UI. This name is passed to the templates.
  • No pr: block: PR validation builds are correctly managed via Branch Policies in the UI, which is the recommended approach.
# [PROJECT-NAME] pipeline.
# Add steps that build, run tests, deploy, and more: https://aka.ms/yaml.

# Runs on pushes (e.g., merges) TO these branches
# This is what runs your Build AND Deploy stages
trigger:
  branches:
    include: ["main", "develop"]
  paths:
    exclude:
      # This file is handled by 'web-[PROJECT-NAME]-docker-image' pipeline
      - 'ci/docker/Dockerfile'

pool:
  vmImage: ubuntu-latest

variables:
  # Convert all variables to the list syntax for consistency
  - name: pool
    value: 'aws-ubuntu-latest'
  - name: CI
    value: 'true'
  - name: PIPELINE_ENV
    value: 'ci'
  - name: mainBranch
    value: ${{ eq(variables['Build.SourceBranch'], 'refs/heads/main') }}
  - name: envBranch
    value: ${{ or(
        eq(variables['Build.SourceBranch'], 'refs/heads/main'),
        eq(variables['Build.SourceBranch'], 'refs/heads/develop'),
        startsWith(variables['Build.SourceBranch'], 'refs/heads/feature/'),
        startsWith(variables['Build.SourceBranch'], 'refs/heads/story/'),
        startsWith(variables['Build.SourceBranch'], 'refs/heads/bug/'),
        startsWith(variables['Build.SourceBranch'], 'refs/heads/task/')
      ) }}
  - name: environment_azure
    # 1. Check for the main branch (for prod)
    ${{ if eq(variables['Build.SourceBranch'], 'refs/heads/main') }}:
      value: '[PROJECT_NAME]-app-prod'
    # 2. Check if it's a PR merge/push (for dev-ci, automatic deploy)
    ${{ elseif eq(variables['Build.Reason'], 'IndividualCI') }}:
      value: '[PROJECT_NAME]-app-dev-ci'
    # 3. All other triggers (like Manual runs) go to the dev env (with deploy approval)
    ${{ else }}:
      value: '[PROJECT_NAME]-app-dev'
  # Define the name of your new image pipeline here
  - name: imageBuilderPipelineName
    value: 'web-[PROJECT_NAME]-docker-image' # PHP, Composer, acli and useful php extensions

stages:
# ======================================================================================
# BUILD STAGE
# ======================================================================================
- stage: Build
  displayName: Build [PROJECT-NAME]
  # ... (variables) ...
  jobs:
    - template: stages/build.yml
      parameters:
        jobName: 'Build_[PROJECT-NAME]'
        displayName: 'Build job'
        rootDirectory: $(rootFolder)
        imageBuilderPipelineDefinitionName: $(imageBuilderPipelineName) # Pass it in

# ======================================================================================
# DEPLOY STAGE
# ======================================================================================
- stage: Deploy
  displayName: Deploy [PROJECT-NAME]
  dependsOn: Build
  # This condition is why we don't need a separate PR pipeline.
  # It ensures deploys ONLY run on merges, not PRs.
  condition: and(not(or(failed(), canceled())), eq(variables['envBranch'], 'true'), ne(variables['Build.Reason'], 'PullRequest'))
  # ... (variables) ...
  jobs:
    - template: stages/deploy.yml
      parameters:
        jobName: "Deploy_[PROJECT-NAME]"
        displayName: "Deploy job"
        environmentAzureDevOps: $(environment_azure)
        privateSSHKeyName: "acquia_[PROJECT-NAME]_ssh"
        # ... (other parameters) ...
        imageBuilderPipelineDefinitionName: $(imageBuilderPipelineName) # Pass it in

Important Environment Setup

This pipeline relies on specific Azure DevOps Environments being configured correctly in the UI:

  1. Navigate to Pipelines -> Environments.
  2. Ensure you have the following environments created:
    • [PROJECT-NAME]-app-prod: This environment should have your required manual Approvals and checks configured (e.g., waiting for specific users or groups).
    • [PROJECT-NAME]-app-dev: This environment should also have your required manual Approvals and checks configured for non-merge deployments.
    • [PROJECT-NAME]-app-dev-ci: This environment must exist, but it MUST NOT have any Approvals and checks configured. This allows the automatic deployment for IndividualCI builds (PR merges/pushes).

This separation allows your YAML logic to route builds correctly: automatic merges go to the -ci environment, while manual runs or merges to main go to environments requiring human sign-off.

The following is an example ci/docker/Dockerfile file:

# Use the officialPHP image
FROM php:8.3-cli

# Ensure all packages are up to date to reduce vulnerabilities
RUN apt-get update && apt-get upgrade -y && apt-get clean && rm -rf /var/lib/apt/lists/*

# Install system dependencies as needed
RUN apt-get update && apt-get install -y \
  # Base packages
  curl git libzip-dev libpng-dev libjpeg-dev libfreetype6-dev libxml2-dev unzip \
  # MySQL client
  default-mysql-client \
  # Required PHP extensions
  && docker-php-ext-install zip gd xml pdo_mysql

# Install Composer
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

# Set PHP memory limit from environment variable, default to -1 (unlimited)
RUN echo "memory_limit = -1" > /usr/local/etc/php/conf.d/memory-limit.ini; \
  # Install Acquia CLI (phar already includes correct shebang)
  curl -fsSL https://github.com/acquia/cli/releases/latest/download/acli.phar -o /usr/local/bin/acli \
  && chmod +x /usr/local/bin/acli

# Set working directory
WORKDIR /app