diff --git a/.github/workflows/deploy-Parameterized.yml b/.github/workflows/deploy-Parameterized.yml index 86358001b..7a693a3cf 100644 --- a/.github/workflows/deploy-Parameterized.yml +++ b/.github/workflows/deploy-Parameterized.yml @@ -68,16 +68,95 @@ env: BUILD_DOCKER_IMAGE: ${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.build_docker_image || false) || false }} jobs: + docker-build: + # ============================================================================ + # DOCKER BUILD JOB - When does it run? + # ============================================================================ + # ✅ RUNS ONLY WHEN: + # - Trigger = Manual (workflow_dispatch) + # AND "Build and push Docker image" checkbox = ✅ CHECKED + # + # ❌ SKIPPED WHEN: + # - Any automatic trigger (Pull Request, Schedule, Workflow Run) + # - Manual trigger but "Build and push Docker image" = ❌ UNCHECKED + # - Manual trigger but checkbox not provided (defaults to false) + # + # SIMPLE RULE: Only builds Docker when you manually trigger AND check the box! + # ============================================================================ + if: github.event_name == 'workflow_dispatch' && github.event.inputs.build_docker_image == 'true' + runs-on: ubuntu-latest + outputs: + IMAGE_TAG: ${{ steps.generate_docker_tag.outputs.IMAGE_TAG }} + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Generate Unique Docker Image Tag + id: generate_docker_tag + run: | + echo "🔨 Building new Docker image - generating unique tag..." + # Generate unique tag for manual deployment runs + TIMESTAMP=$(date +%Y%m%d-%H%M%S) + RUN_ID="${{ github.run_id }}" + BRANCH_NAME="${{ github.head_ref || github.ref_name }}" + # Sanitize branch name for Docker tag (replace invalid characters with hyphens) + CLEAN_BRANCH_NAME=$(echo "$BRANCH_NAME" | sed 's/[^a-zA-Z0-9._-]/-/g' | sed 's/--*/-/g' | sed 's/^-\|-$//g') + UNIQUE_TAG="${CLEAN_BRANCH_NAME}-${TIMESTAMP}-${RUN_ID}" + echo "IMAGE_TAG=$UNIQUE_TAG" >> $GITHUB_ENV + echo "IMAGE_TAG=$UNIQUE_TAG" >> $GITHUB_OUTPUT + echo "Generated unique Docker tag: $UNIQUE_TAG" + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Azure Container Registry + uses: azure/docker-login@v2 + with: + login-server: ${{ secrets.ACR_TEST_LOGIN_SERVER }} + username: ${{ secrets.ACR_TEST_USERNAME }} + password: ${{ secrets.ACR_TEST_PASSWORD }} + + - name: Build and Push Docker Image + id: build_push_image + uses: docker/build-push-action@v6 + with: + context: ./src + file: ./src/WebApp.Dockerfile + push: true + tags: | + ${{ secrets.ACR_TEST_LOGIN_SERVER }}/webapp:${{ steps.generate_docker_tag.outputs.IMAGE_TAG }} + ${{ secrets.ACR_TEST_LOGIN_SERVER }}/webapp:${{ steps.generate_docker_tag.outputs.IMAGE_TAG }}_${{ github.run_number }} + + - name: Verify Docker Image Build + run: | + echo "✅ Docker image successfully built and pushed" + echo "Image tag: ${{ steps.generate_docker_tag.outputs.IMAGE_TAG }}" + echo "Run number: ${{ github.run_number }}" + deploy: - # Skip deployment if an existing webapp URL is provided - if: github.event_name != 'workflow_dispatch' || github.event.inputs.existing_webapp_url == '' || github.event.inputs.existing_webapp_url == null + # ============================================================================ + # DEPLOY JOB - When does it run? + # ============================================================================ + # ✅ RUNS WHEN: + # - ANY automatic trigger (Pull Request, Schedule, Workflow Run) = ALWAYS RUNS + # - Manual trigger with "Existing WebApp URL" = EMPTY/BLANK + # - Manual trigger with "Existing WebApp URL" = NOT PROVIDED + # + # ❌ SKIPPED WHEN: + # - Manual trigger with "Existing WebApp URL" = PROVIDED/FILLED + # (Why skip? Because you want to test existing app, not deploy new one!) + # + # SIMPLE RULE: Deploys new infrastructure unless you provide existing webapp URL! + # ============================================================================ + if: always() && (github.event_name != 'workflow_dispatch' || github.event.inputs.existing_webapp_url == '' || github.event.inputs.existing_webapp_url == null) + needs: [docker-build] # Add dependency on docker-build job (will be skipped if not needed) runs-on: ubuntu-latest outputs: RESOURCE_GROUP_NAME: ${{ steps.check_create_rg.outputs.RESOURCE_GROUP_NAME }} WEBAPP_URL: ${{ steps.get_output.outputs.WEBAPP_URL }} ENV_NAME: ${{ steps.generate_env_name.outputs.ENV_NAME }} AZURE_LOCATION: ${{ steps.set_region.outputs.AZURE_LOCATION }} - IMAGE_TAG: ${{ steps.generate_docker_tag.outputs.IMAGE_TAG }} + IMAGE_TAG: ${{ steps.determine_image_tag.outputs.IMAGE_TAG }} env: # For automatic triggers: force Non-WAF + Non-EXP, for manual dispatch: use inputs WAF_ENABLED: ${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.waf_enabled || false) || false }} @@ -245,21 +324,18 @@ jobs: echo "SOLUTION_PREFIX=${UNIQUE_SOLUTION_PREFIX}" >> $GITHUB_ENV echo "Generated SOLUTION_PREFIX: ${UNIQUE_SOLUTION_PREFIX}" - - name: Generate Unique Docker Image Tag - id: generate_docker_tag + - name: Determine Docker Image Tag + id: determine_image_tag run: | if [[ "${{ env.BUILD_DOCKER_IMAGE }}" == "true" ]]; then - echo "🔨 Building new Docker image - generating unique tag..." - # Generate unique tag for manual deployment runs - TIMESTAMP=$(date +%Y%m%d-%H%M%S) - RUN_ID="${{ github.run_id }}" - BRANCH_NAME="${{ env.BRANCH_NAME }}" - # Sanitize branch name for Docker tag (replace invalid characters with hyphens) - CLEAN_BRANCH_NAME=$(echo "$BRANCH_NAME" | sed 's/[^a-zA-Z0-9._-]/-/g' | sed 's/--*/-/g' | sed 's/^-\|-$//g') - UNIQUE_TAG="${CLEAN_BRANCH_NAME}-${TIMESTAMP}-${RUN_ID}" - echo "IMAGE_TAG=$UNIQUE_TAG" >> $GITHUB_ENV - echo "IMAGE_TAG=$UNIQUE_TAG" >> $GITHUB_OUTPUT - echo "Generated unique Docker tag: $UNIQUE_TAG" + # Use the tag from docker-build job if it was built + if [[ "${{ needs.docker-build.result }}" == "success" ]]; then + IMAGE_TAG="${{ needs.docker-build.outputs.IMAGE_TAG }}" + echo "🔗 Using Docker image tag from build job: $IMAGE_TAG" + else + echo "❌ Docker build job failed or was skipped, but BUILD_DOCKER_IMAGE is true" + exit 1 + fi else echo "🏷️ Using existing Docker image based on branch..." BRANCH_NAME="${{ env.BRANCH_NAME }}" @@ -280,41 +356,11 @@ jobs: echo "Using default for branch '$BRANCH_NAME' - image tag: latest_waf" fi - echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV - echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_OUTPUT echo "Using existing Docker image tag: $IMAGE_TAG" fi - - - name: Set up Docker Buildx - if: env.BUILD_DOCKER_IMAGE == 'true' - uses: docker/setup-buildx-action@v3 - - - name: Log in to Azure Container Registry - if: env.BUILD_DOCKER_IMAGE == 'true' - uses: azure/docker-login@v2 - with: - login-server: ${{ secrets.ACR_TEST_LOGIN_SERVER }} - username: ${{ secrets.ACR_TEST_USERNAME }} - password: ${{ secrets.ACR_TEST_PASSWORD }} - - - name: Build and Push Docker Image - if: env.BUILD_DOCKER_IMAGE == 'true' - id: build_push_image - uses: docker/build-push-action@v6 - with: - context: ./src - file: ./src/WebApp.Dockerfile - push: true - tags: | - ${{ secrets.ACR_TEST_LOGIN_SERVER }}/webapp:${{ steps.generate_docker_tag.outputs.IMAGE_TAG }} - ${{ secrets.ACR_TEST_LOGIN_SERVER }}/webapp:${{ steps.generate_docker_tag.outputs.IMAGE_TAG }}_${{ github.run_number }} - - - name: Verify Docker Image Build - if: env.BUILD_DOCKER_IMAGE == 'true' - run: | - echo "✅ Docker image successfully built and pushed" - echo "Image tag: ${{ env.IMAGE_TAG }}" - echo "Run number: ${{ github.run_number }}" + + echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV + echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_OUTPUT - name: Generate Unique Environment Name id: generate_env_name @@ -345,9 +391,9 @@ jobs: - name: Display Docker Image Tag run: | echo "=== Docker Image Information ===" - echo "Docker Image Tag: ${{ env.IMAGE_TAG }}" + echo "Docker Image Tag: ${{ steps.determine_image_tag.outputs.IMAGE_TAG }}" echo "Registry: ${{ secrets.ACR_TEST_LOGIN_SERVER }}" - echo "Full Image: ${{ secrets.ACR_TEST_LOGIN_SERVER }}/webapp:${{ env.IMAGE_TAG }}" + echo "Full Image: ${{ secrets.ACR_TEST_LOGIN_SERVER }}/webapp:${{ steps.determine_image_tag.outputs.IMAGE_TAG }}" echo "================================" - name: Deploy using azd up and extract values (${{ github.event.inputs.waf_enabled == 'true' && 'WAF' || 'Non-WAF' }}+${{ (github.event.inputs.EXP == 'true' || github.event.inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID != '' || github.event.inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID != '') && 'EXP' || 'Non-EXP' }}) @@ -357,7 +403,7 @@ jobs: echo "Starting azd deployment..." echo "WAF Enabled: ${{ env.WAF_ENABLED }}" echo "EXP: ${{ env.EXP }}" - echo "Using Docker Image Tag: ${{ env.IMAGE_TAG }}" + echo "Using Docker Image Tag: ${{ steps.determine_image_tag.outputs.IMAGE_TAG }}" # Install azd (Azure Developer CLI) curl -fsSL https://aka.ms/install-azd.sh | bash @@ -376,7 +422,7 @@ jobs: azd env set AZURE_SUBSCRIPTION_ID="${{ secrets.AZURE_SUBSCRIPTION_ID }}" azd env set AZURE_ENV_OPENAI_LOCATION="$AZURE_LOCATION" azd env set AZURE_RESOURCE_GROUP="$RESOURCE_GROUP_NAME" - azd env set AZURE_ENV_IMAGETAG="${{ env.IMAGE_TAG }}" + azd env set AZURE_ENV_IMAGETAG="${{ steps.determine_image_tag.outputs.IMAGE_TAG }}" azd env set AZURE_DEV_COLLECT_TELEMETRY="no" # Set ACR name only when building Docker image @@ -434,39 +480,19 @@ jobs: echo "AI_FOUNDRY_RESOURCE_ID=$AI_FOUNDRY_RESOURCE_ID" >> $GITHUB_ENV export AI_SEARCH_SERVICE_NAME=$(echo "$DEPLOY_OUTPUT" | jq -r '.AI_SEARCH_SERVICE_NAME // empty') echo "AI_SEARCH_SERVICE_NAME=$AI_SEARCH_SERVICE_NAME" >> $GITHUB_ENV - export COSMOS_DB_ACCOUNT_NAME=$(echo "$DEPLOY_OUTPUT" | jq -r '.COSMOS_DB_ACCOUNT_NAME // empty') - echo "COSMOS_DB_ACCOUNT_NAME=$COSMOS_DB_ACCOUNT_NAME" >> $GITHUB_ENV + export AZURE_COSMOSDB_ACCOUNT=$(echo "$DEPLOY_OUTPUT" | jq -r '.AZURE_COSMOSDB_ACCOUNT // empty') + echo "AZURE_COSMOSDB_ACCOUNT=$AZURE_COSMOSDB_ACCOUNT" >> $GITHUB_ENV export STORAGE_ACCOUNT_NAME=$(echo "$DEPLOY_OUTPUT" | jq -r '.STORAGE_ACCOUNT_NAME // empty') echo "STORAGE_ACCOUNT_NAME=$STORAGE_ACCOUNT_NAME" >> $GITHUB_ENV export STORAGE_CONTAINER_NAME=$(echo "$DEPLOY_OUTPUT" | jq -r '.STORAGE_CONTAINER_NAME // empty') echo "STORAGE_CONTAINER_NAME=$STORAGE_CONTAINER_NAME" >> $GITHUB_ENV export KEY_VAULT_NAME=$(echo "$DEPLOY_OUTPUT" | jq -r '.KEY_VAULT_NAME // empty') echo "KEY_VAULT_NAME=$KEY_VAULT_NAME" >> $GITHUB_ENV - export SQL_SERVER_NAME=$(echo "$DEPLOY_OUTPUT" | jq -r '.SQLDB_SERVER_NAME // empty') - echo "SQL_SERVER_NAME=$SQL_SERVER_NAME" >> $GITHUB_ENV - export SQL_DATABASE=$(echo "$DEPLOY_OUTPUT" | jq -r '.SQLDB_DATABASE // empty') - echo "SQL_DATABASE=$SQL_DATABASE" >> $GITHUB_ENV - export CLIENT_ID=$(echo "$DEPLOY_OUTPUT" | jq -r '.MANAGEDIDENTITY_SQL_CLIENTID // empty') - echo "CLIENT_ID=$CLIENT_ID" >> $GITHUB_ENV - export CLIENT_NAME=$(echo "$DEPLOY_OUTPUT" | jq -r '.MANAGEDIDENTITY_SQL_NAME // empty') - echo "CLIENT_NAME=$CLIENT_NAME" >> $GITHUB_ENV - export MANAGEDIDENTITY_WEBAPP_CLIENTID=$(echo "$DEPLOY_OUTPUT" | jq -r '.MANAGEDIDENTITY_WEBAPP_CLIENTID // empty') - echo "MANAGEDIDENTITY_WEBAPP_CLIENTID=$MANAGEDIDENTITY_WEBAPP_CLIENTID" >> $GITHUB_ENV export RESOURCE_GROUP_NAME=$(echo "$DEPLOY_OUTPUT" | jq -r '.RESOURCE_GROUP_NAME // .AZURE_RESOURCE_GROUP // empty') [[ -z "$RESOURCE_GROUP_NAME" ]] && export RESOURCE_GROUP_NAME="$RESOURCE_GROUP_NAME" echo "RESOURCE_GROUP_NAME=$RESOURCE_GROUP_NAME" >> $GITHUB_ENV WEBAPP_URL=$(echo "$DEPLOY_OUTPUT" | jq -r '.WEB_APP_URL // .SERVICE_BACKEND_ENDPOINT_URL // empty') echo "WEBAPP_URL=$WEBAPP_URL" >> $GITHUB_OUTPUT - WEB_APP_NAME=$(echo "$DEPLOY_OUTPUT" | jq -r '.WEB_APP_NAME // .SERVICE_BACKEND_NAME // empty') - echo "WEB_APP_NAME=$WEB_APP_NAME" >> $GITHUB_ENV - - # echo "🔧 Disabling AUTH_ENABLED for the web app..." - # if [[ -n "$WEB_APP_NAME" && -n "$RESOURCE_GROUP_NAME" ]]; then - # az webapp config appsettings set -g "$RESOURCE_GROUP_NAME" -n "$WEB_APP_NAME" --settings AUTH_ENABLED=false - # else - # echo "Warning: Could not disable AUTH_ENABLED - WEB_APP_NAME or RESOURCE_GROUP_NAME not found" - # fi - sleep 30 - name: Run Post-Deployment Script @@ -483,7 +509,7 @@ jobs: "$STORAGE_ACCOUNT_NAME" \ "$STORAGE_CONTAINER_NAME" \ "$KEY_VAULT_NAME" \ - "$COSMOS_DB_ACCOUNT_NAME" \ + "$AZURE_COSMOSDB_ACCOUNT" \ "$RESOURCE_GROUP_NAME" \ "$AI_SEARCH_SERVICE_NAME" \ "${{ secrets.AZURE_CLIENT_ID }}" \ @@ -495,10 +521,54 @@ jobs: az logout echo "Logged out from Azure." + - name: Send Notification + if: always() + run: | + RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + WEBAPP_URL="${{ steps.get_output.outputs.WEBAPP_URL }}" + RESOURCE_GROUP="${{ steps.check_create_rg.outputs.RESOURCE_GROUP_NAME }}" + IMAGE_TAG="${{ steps.determine_image_tag.outputs.IMAGE_TAG }}" + IS_SUCCESS=${{ job.status == 'success' }} + + # Construct the email body based on deployment result + if [ "$IS_SUCCESS" = "true" ]; then + EMAIL_BODY=$(cat <Dear Team,

We would like to inform you that the DocGen deployment process has completed successfully.

Deployment Details:
• Resource Group: ${RESOURCE_GROUP}
• Web App URL: ${WEBAPP_URL}

Run URL: ${RUN_URL}

The application is now ready for use.

Best regards,
Your Automation Team

", + "subject": "DocGen Deployment - Success" + } + EOF + ) + else + EMAIL_BODY=$(cat <Dear Team,

We would like to inform you that the DocGen deployment process has encountered an issue and has failed to complete successfully.

Deployment Details:
• Resource Group: ${RESOURCE_GROUP}

Run URL: ${RUN_URL}

Please investigate the matter at your earliest convenience.

Best regards,
Your Automation Team

", + "subject": "DocGen Deployment - Failure" + } + EOF + ) + fi + # Send the notification + curl -X POST "${{ secrets.LOGIC_APP_URL }}" \ + -H "Content-Type: application/json" \ + -d "$EMAIL_BODY" || echo "Failed to send notification" + e2e-test: - # Run e2e tests only when deploy succeeds or when using existing webapp URL - # Skip if deploy job failed (for both automatic and manual triggers) - # Respect the run_e2e_tests checkbox - skip if explicitly unchecked + # ============================================================================ + # E2E TEST JOB - When does it run? + # ============================================================================ + # ✅ RUNS WHEN: + # - Deploy job succeeded AND "Run end-to-end tests" = ✅ CHECKED (default) + # - Manual trigger with "Existing WebApp URL" provided AND tests = ✅ CHECKED + # - Any automatic trigger (tests always run by default if deploy succeeds) + # + # ❌ SKIPPED WHEN: + # - Deploy job failed or was skipped + # - Manual trigger with "Run end-to-end tests" = ❌ UNCHECKED + # - No webapp available (neither deployed nor existing URL provided) + # + # SIMPLE RULE: Tests run against any available webapp unless you uncheck the box! + # ============================================================================ if: (needs.deploy.result == 'success' || github.event.inputs.existing_webapp_url != '') && (github.event_name != 'workflow_dispatch' || github.event.inputs.run_e2e_tests == 'true' || github.event.inputs.run_e2e_tests == null) needs: [deploy] uses: ./.github/workflows/test-automation.yml @@ -507,10 +577,25 @@ jobs: secrets: inherit cleanup-deployment: - # Cleanup when deploy succeeded and cleanup is not explicitly disabled - # Skip if webapp URL is provided (no resources to cleanup) or cleanup checkbox is unchecked - if: needs.deploy.result == 'success' && needs.deploy.outputs.RESOURCE_GROUP_NAME != '' && github.event.inputs.existing_webapp_url == '' && (github.event_name != 'workflow_dispatch' || github.event.inputs.cleanup_resources == 'true' || github.event.inputs.cleanup_resources == null) - needs: [deploy, e2e-test] + # ============================================================================ + # CLEANUP JOB - When does it run? + # ============================================================================ + # ✅ RUNS WHEN: + # - Deploy job succeeded (created new resources to clean up) + # - Resource group was created (has RESOURCE_GROUP_NAME output) + # - "Existing WebApp URL" = EMPTY (means we deployed new stuff to clean) + # - "Cleanup deployed resources" = ✅ CHECKED (default) OR not manual trigger + # + # ❌ SKIPPED WHEN: + # - Deploy job failed (nothing to clean up) + # - "Existing WebApp URL" provided (didn't deploy new resources) + # - Manual trigger with "Cleanup deployed resources" = ❌ UNCHECKED + # - No resource group name available (deployment didn't create resources) + # + # SIMPLE RULE: Cleans up newly deployed resources unless you uncheck the box! + # ============================================================================ + if: always() && needs.deploy.result == 'success' && needs.deploy.outputs.RESOURCE_GROUP_NAME != '' && github.event.inputs.existing_webapp_url == '' && (github.event_name != 'workflow_dispatch' || github.event.inputs.cleanup_resources == 'true' || github.event.inputs.cleanup_resources == null) + needs: [docker-build, deploy, e2e-test] runs-on: ubuntu-latest env: RESOURCE_GROUP_NAME: ${{ needs.deploy.outputs.RESOURCE_GROUP_NAME }} @@ -546,22 +631,33 @@ jobs: set -e echo "🗑️ Cleaning up Docker images from Azure Container Registry..." - if [[ -n "${{ env.IMAGE_TAG }}" && "${{ env.IMAGE_TAG }}" != "latest_waf" ]]; then - echo "Deleting Docker images with tag: ${{ env.IMAGE_TAG }}" + # Determine the image tag to delete - check if docker-build job ran + if [[ "${{ needs.docker-build.result }}" == "success" ]]; then + IMAGE_TAG="${{ needs.docker-build.outputs.IMAGE_TAG }}" + echo "Using image tag from docker-build job: $IMAGE_TAG" + else + IMAGE_TAG="${{ needs.deploy.outputs.IMAGE_TAG }}" + echo "Using image tag from deploy job: $IMAGE_TAG" + fi + + if [[ -n "$IMAGE_TAG" && "$IMAGE_TAG" != "latest_waf" && "$IMAGE_TAG" != "dev" && "$IMAGE_TAG" != "demo" ]]; then + echo "Deleting Docker images with tag: $IMAGE_TAG" # Delete the main image - echo "Deleting image: ${{ secrets.ACR_TEST_LOGIN_SERVER }}/webapp:${{ env.IMAGE_TAG }}" + echo "Deleting image: ${{ secrets.ACR_TEST_LOGIN_SERVER }}/webapp:$IMAGE_TAG" az acr repository delete --name $(echo "${{ secrets.ACR_TEST_LOGIN_SERVER }}" | cut -d'.' -f1) \ - --image webapp:${{ env.IMAGE_TAG }} --yes || echo "Warning: Failed to delete main image or image not found" + --image webapp:$IMAGE_TAG --yes || echo "Warning: Failed to delete main image or image not found" - # Delete the image with run number suffix - echo "Deleting image: ${{ secrets.ACR_TEST_LOGIN_SERVER }}/webapp:${{ env.IMAGE_TAG }}_${{ github.run_number }}" - az acr repository delete --name $(echo "${{ secrets.ACR_TEST_LOGIN_SERVER }}" | cut -d'.' -f1) \ - --image webapp:${{ env.IMAGE_TAG }}_${{ github.run_number }} --yes || echo "Warning: Failed to delete run-numbered image or image not found" + # Delete the image with run number suffix (only if it was a custom build) + if [[ "${{ needs.docker-build.result }}" == "success" ]]; then + echo "Deleting image: ${{ secrets.ACR_TEST_LOGIN_SERVER }}/webapp:${IMAGE_TAG}_${{ github.run_number }}" + az acr repository delete --name $(echo "${{ secrets.ACR_TEST_LOGIN_SERVER }}" | cut -d'.' -f1) \ + --image webapp:${IMAGE_TAG}_${{ github.run_number }} --yes || echo "Warning: Failed to delete run-numbered image or image not found" + fi echo "✅ Docker images cleanup completed" else - echo "⚠️ Skipping Docker image cleanup (using latest_waf or no custom image tag)" + echo "⚠️ Skipping Docker image cleanup (using standard branch image: $IMAGE_TAG)" fi - name: Select Environment