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_azurevariable uses compile-time expressions (${{ ... }}) to dynamically choose the target Azure DevOps Environment.- Pushes to
maintarget[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).
- Pushes to
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 inImportant Environment Setup
This pipeline relies on specific Azure DevOps Environments being configured correctly in the UI:
- Navigate to Pipelines -> Environments.
- 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 forIndividualCIbuilds (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