CD yml - elyjhone23/ado-pipe GitHub Wiki
# ─────────────────────────────────────────────────────────────────────────────
# MyDodiTech — CD Pipeline
# Azure DevOps Release Pipeline (YAML)
#
# Prerequisites:
# 1. Azure DevOps Environment "PRD" created:
# Pipelines => Environments => New environment => name it "PRD"
# 2. VM registered as a resource inside the PRD environment:
# Open PRD environment => Add resource => Virtual machine => follow the
# registration script (installs the self-hosted agent on the VM).
# The agent runs AS the VM — filesystem access is local (no rsync/SSH).
# 3. Approval gate configured on the PRD environment:
# PRD environment => Approvals and checks => Approvals => add approvers.
# 4. Runtime image built ONCE on the VM:
# docker build -t ddt/dotnet:10.0.103 /path/to/infrastructure-repo/deploy/
# Only rebuild when upgrading the .NET framework version.
# 5. Variable group "mydoditech-prd-secrets" created in Pipelines => Library.
# Required variables — see Variables section below.
#
# Deployment flow:
# Download artifact => FileTransform appsettings.prd.json => Stop webapp
# => Copy files to volume => Start webapp => Health check
# ─────────────────────────────────────────────────────────────────────────────
trigger: none # CD is triggered manually from the Azure DevOps UI.
# To auto-trigger on CI completion: Edit this pipeline =>
# Triggers tab => Pipeline completion triggers => add CI pipeline.
variables:
# ── CI pipeline reference ─────────────────────────────────────────────────
# Set this to the exact name shown at the top of the CI pipeline page:
# Azure DevOps => Pipelines => Pipelines => click the CI pipeline => page title
- name: ciPipelineName
value: "a008-dodifin-ci" # <= Should match the CI pipeline name exactly
# ── Paths (no secrets) ────────────────────────────────────────────────────
- name: artifactName
value: "mydoditech-web"
- name: dockerComposeDir
value: "/home/ddt25/infrastructure/apps/dodifin"
- name: deployPath
value: "$(dockerComposeDir)/mounts/app"
- name: deployPublishPath
value: "$(deployPath)/publish"
- name: dotnetVersion
value: "10.0.x"
# ── Secret Variable Group ─────────────────────────────────────────────────
# All secrets come from Azure DevOps Library variable group.
# Required variables in "mydoditech-prd-secrets":
#
# Database:
# POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB
#
# Keycloak SSO:
# KEYCLOAK_BASE_URL, KEYCLOAK_REALM, KEYCLOAK_CLIENT_ID, KEYCLOAK_CLIENT_SECRET
#
# Email:
# EMAIL_SMTP_HOST, EMAIL_FROM_ADDRESS, EMAIL_FROM_NAME
#
# Multi-tenancy:
# APP_MAIN_DOMAIN, APP_DEFAULT_TENANT, APP_ALLOWED_HOSTS
#
# Invitations:
# SUPPORT_EMAIL, COMPANY_NAME
#
# Quotation signatures:
# QUOTATION_TOKEN_SECRET_KEY
#
# Deployment:
# APP_VOLUME_PATH (e.g. /home/user/infrastructure/ddt-apps/mydoditech/app)
#
# Variable names must match exactly the #{TOKEN}# placeholders in
# appsettings.prd.json so that FileTransform@2 can replace them.
- group: mydoditech-prd-secrets
stages:
# ────────────────────────────────────────────────────────────────────────────
# Stage 1 — Deploy to Production (PRD)
#
# Uses Azure DevOps Environment "PRD" with a VM resource.
# The agent IS the production VM — file copies are local filesystem operations.
# Approval gates are configured in the PRD environment UI.
# ────────────────────────────────────────────────────────────────────────────
- stage: deploy_prd
displayName: "Deploy to PRD"
jobs:
- deployment: deploy
displayName: "Deploy Web App"
# Targets the PRD environment — gates, approvals, and VM resource are
# configured there. The agent that runs the job IS the registered VM.
environment: "PRD"
timeoutInMinutes: 15
strategy:
runOnce:
deploy:
steps:
# ── Download artifact from CI ────────────────────────────────
- task: DownloadPipelineArtifact@2
displayName: "Download artifact: $(artifactName)"
inputs:
buildType: specific
project: "$(System.TeamProjectId)"
definition: "$(ciPipelineName)"
buildVersionToDownload: latestFromBranch
branchName: refs/heads/dev
artifactName: $(artifactName)
targetPath: "$(Pipeline.Workspace)/artifact"
# ── FileTransform — replace #{TOKEN}# in appsettings.prd.json ─
# Pipeline variables (from the variable group above) whose names
# match a #{TOKEN}# placeholder are substituted automatically.
- task: FileTransform@2
displayName: "Transform appsettings.prd.json tokens"
inputs:
folderPath: "$(Pipeline.Workspace)/artifact"
xmlTransformationRules: ""
jsonTargetFiles: "**/appsettings.prd.json"
# ── Write .env file for docker-compose ──────────────────────
# The .env file provides PostgreSQL credentials to the postgres
# service only. App configuration comes from appsettings.prd.json.
- script: |
cat > "$(dockerComposeDir)/.env" << 'ENVEOF'
APP_VOLUME_PATH=$(APP_VOLUME_PATH)
POSTGRES_DB=$(POSTGRES_DB)
POSTGRES_USER=$(POSTGRES_USER)
POSTGRES_PASSWORD=$(POSTGRES_PASSWORD)
ENVEOF
chmod 600 "$(dockerComposeDir)/.env"
echo ".env written"
displayName: "Write .env file (postgres credentials)"
# ── Stop webapp container ────────────────────────────────────
- script: |
cd "$(dockerComposeDir)"
docker compose stop webapp
echo "webapp container stopped"
displayName: "Stop webapp container"
# ── Ensure volume directories exist ─────────────────────────
- script: |
mkdir -p "$(deployPublishPath)"
mkdir -p "$(deployPath)/logs"
mkdir -p "$(deployPath)/data/documents"
mkdir -p "$(deployPath)/data/uploads"
echo "Volume directories ensured"
displayName: "Ensure volume directories exist"
# ── Deploy artifact to volume ────────────────────────────────
# Local copy — the agent runs on the VM, no rsync/SSH needed.
# appsettings.prd.json is already token-replaced (FileTransform above).
- script: |
cp -a "$(Pipeline.Workspace)/artifact/." "$(deployPublishPath)/"
chmod 640 "$(deployPublishPath)/appsettings.prd.json"
echo "Artifact deployed to $(deployPublishPath)"
ls -la "$(deployPublishPath)/" | head -20
displayName: "Copy artifact to volume (local)"
# ── Start webapp container ───────────────────────────────────
- script: |
cd "$(dockerComposeDir)"
docker compose start webapp
echo "webapp container started"
displayName: "Start webapp container"
# ── Health check — wait for app to be ready ──────────────────
- script: |
echo "Waiting for application to be ready..."
MAX_RETRIES=12
RETRY_DELAY=5
APP_URL="http://localhost:8080"
for i in $(seq 1 $MAX_RETRIES); do
STATUS=$(curl --silent --output /dev/null --write-out "%{http_code}" "$APP_URL" || echo "000")
echo "Attempt $i/$MAX_RETRIES — HTTP $STATUS"
if [ "$STATUS" = "200" ] || [ "$STATUS" = "302" ]; then
echo "Application is healthy (HTTP $STATUS)"
exit 0
fi
sleep $RETRY_DELAY
done
echo "ERROR: Application did not become healthy after $MAX_RETRIES retries"
docker compose -f "$(dockerComposeDir)/docker-compose.yml" logs webapp --tail=50
exit 1
displayName: "Health check"