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"