This is the most complex template. It downloads the "Locked Box" and then runs a full integration test before zipping up the final code artifact.
Key Features:
DownloadPipelineArtifact@2: This task downloads thephp-build-[PROJECT-NAME]artifact from the "Factory" pipeline (buildType: 'specific').gunzip -c ... | docker load: This is the other half of ourgzipoptimization. It decompresses the image in memory and pipes it directly intodocker load. This is faster than writing the uncompressed file to disk first.- Manual Docker Networking: This is the most critical part. We cannot use the
services:block because it creates a race condition (it runs beforesteps:). The only stable way to get Docker-in-Docker containers to talk is:- Manually create a network:
docker network create ci-net. - Manually start
mysql:docker run --name mysql --network=ci-net .... - Manually attach all other containers:
docker run --network=ci-net .... This adds 15 seconds but guarantees a 100% stable build, which is a worthwhile trade.
- Manually create a network:
- Inclusion
zip: Thezipcommand explicitly lists only the folders needed for deployment. This is safer than an exclusion-based approach, as it ensures no junk files (.DS_Store,.idea) ever make it into the artifact.
# build.yml
parameters:
displayName: 'Build Drupal'
jobName: 'Build_Drupal'
agentPool: 'aws-ubuntu-latest'
rootDirectory: '$(Build.SourcesDirectory)'
phpVersion: '8.3' # No longer used to pull, but good for reference
phpImageName: 'php-build-[PROJECT-NAME]'
# This new parameter is required - pass the name of your new image-builder pipeline.
imageBuilderPipelineDefinitionName: ''
jobs:
- job: ${{ parameters.jobName }}
displayName: ${{ parameters.displayName }}
pool: ${{ parameters.agentPool }}
workspace:
clean: all
steps:
- checkout: self
submodules: true
persistCredentials: true
fetchDepth: 0 # Disables shallow fetch
# Download the "Locked Box"
- task: DownloadPipelineArtifact@2
displayName: 'Download Docker Image Artifact'
inputs:
buildType: 'specific'
project: '$(System.TeamProjectId)'
definition: '${{ parameters.imageBuilderPipelineDefinitionName }}'
buildVersionToDownload: 'latest'
artifactName: '${{ parameters.phpImageName }}'
targetPath: '$(Pipeline.Workspace)/docker-artifact'
- script: |
# Use gunzip to decompress and pipe to docker load
gunzip -c $(Pipeline.Workspace)/docker-artifact/${{ parameters.phpImageName }}.tar.gz | docker load
displayName: 'Load Docker Image'
# === Manual Docker Networking ===
# This is the only stable way to get Docker-in-Docker containers to talk.
- script: 'docker network create ci-net || true'
displayName: 'Create Docker Network'
- task: Cache@2
displayName: 'Cache Composer dependencies'
inputs:
key: 'composer | "$(Agent.OS)" | composer.lock'
restoreKeys: composer | "$(Agent.OS)"
path: '$(Build.SourcesDirectory)/vendor'
- script: |
docker run --rm --network=ci-net -e CI=true -e PIPELINE_ENV=ci -v $(Build.SourcesDirectory):/app -w /app ${{ parameters.phpImageName }} bash -c "composer validate --no-check-all --ansi && composer install --optimize-autoloader"
displayName: 'Install dependencies (Composer)'
- task: Bash@3
displayName: 'Validate code'
inputs:
targetType: 'inline'
script: |
docker run --rm --network=ci-net -e CI=true -e PIPELINE_ENV=ci -v $(Build.SourcesDirectory):/app -w /app ${{ parameters.phpImageName }} bash ci/scripts/build/validate.sh
env:
COMPOSER_ALLOW_SUPERUSER: 1
# Manually start MySQL and attach it to our network
- script: |
docker rm -f mysql || true
docker run --name mysql --network=ci-net -e CI=true -e PIPELINE_ENV=ci -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=drupal -d mysql:8.0
echo "Waiting for MySQL to initialize..."
for i in {1..30}; do
if docker exec mysql mysqladmin ping -uroot -proot --silent; then
echo "MySQL is up!"
break
fi
sleep 2
done
displayName: 'Start MySQL Docker Container'
# Run integration tests, which can now find 'mysql' on the 'ci-net' network
- task: Bash@3
displayName: 'Prepare MySQL for usage'
inputs:
targetType: 'inline'
script: |
docker run --rm --network=ci-net -e CI=true -e PIPELINE_ENV=ci -v $(Build.SourcesDirectory):/app -w /app ${{ parameters.phpImageName }} bash ci/scripts/build/mysql.sh
- task: Bash@3
displayName: 'Install web app & apply configs'
inputs:
targetType: 'inline'
script: |
docker run --rm --network=ci-net -e CI=true -e PIPELINE_ENV=ci -v $(Build.SourcesDirectory):/app -w /app ${{ parameters.phpImageName }} bash -c "
export PATH=\"/app/vendor/bin:\$PATH\" && \
export MYSQL_CLIENT_FLAGS="--ssl-mode=DISABLED" && \
composer drupal:install --no-interaction --verbose && \
./vendor/bin/drush -y updb && \
./vendor/bin/drush -y cim"
env:
COMPOSER_ALLOW_SUPERUSER: 1
CI: 'true'
PIPELINE_ENV: 'ci'
# Build the final application artifact
- script: |
set -e
cd $(Build.SourcesDirectory)
mkdir -p $(Build.ArtifactStagingDirectory)
# Your safer, inclusion-based zip command
zip -r $(Build.ArtifactStagingDirectory)/$(Build.Repository.Name).zip \
.gitignore \
composer.json \
composer.lock \
salt.txt \
ci \
config \
docroot \
hooks
displayName: 'Build Artifact'
# Publish the final application artifact
- task: PublishBuildArtifacts@1
displayName: 'Publish Build Artifact'
condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest'))
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)/$(Build.Repository.Name).zip'
ArtifactName: '$(Build.Repository.Name)'