From 0f98e04eced4670970eaaa48ed16c43ce72f719b Mon Sep 17 00:00:00 2001 From: Rafi-Microsoft Date: Tue, 9 Dec 2025 13:20:58 +0530 Subject: [PATCH 01/49] Added banner in readme for archival --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 826f60e9..f8c8a90b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,9 @@ +
+

+ ⚠️ This repository is no longer maintained ⚠️ +

+
+ # Document knowledge mining solution accelerator Ingest, extract, and classify content from a high volume of assets to gain deeper insights and generate relevant suggestions for quick and easy reasoning. This enables the ability to conduct chat-based insight discovery, analysis, and receive suggested prompt guidance to further explore your data. From c31e8825910c8bbbd5a71fea9e19e74d3b6e578a Mon Sep 17 00:00:00 2001 From: Prajwal-Microsoft Date: Fri, 12 Dec 2025 09:58:51 +0530 Subject: [PATCH 02/49] docs: Remove unmaintained repository warning Removed maintenance notice from README. --- README.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/README.md b/README.md index f8c8a90b..826f60e9 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,3 @@ -
-

- ⚠️ This repository is no longer maintained ⚠️ -

-
- # Document knowledge mining solution accelerator Ingest, extract, and classify content from a high volume of assets to gain deeper insights and generate relevant suggestions for quick and easy reasoning. This enables the ability to conduct chat-based insight discovery, analysis, and receive suggested prompt guidance to further explore your data. From 38151c98a504ffe4a7ff77ec0b2809a24880b97b Mon Sep 17 00:00:00 2001 From: Rafi-Microsoft Date: Mon, 15 Dec 2025 17:36:02 +0530 Subject: [PATCH 03/49] pipelines creation initial version v1 --- .github/workflows/deploy-orchestrator.yml | 138 +++++++ .github/workflows/deploy-windows.yml | 100 +++++ .github/workflows/job-cleanup-deployment.yml | 114 ++++++ .github/workflows/job-deploy-windows.yml | 262 ++++++++++++++ .github/workflows/job-deploy.yml | 362 +++++++++++++++++++ .github/workflows/job-docker-build.yml | 99 +++++ .github/workflows/job-send-notification.yml | 224 ++++++++++++ .github/workflows/test-automation-v2.yml | 185 ++++++++++ 8 files changed, 1484 insertions(+) create mode 100644 .github/workflows/deploy-orchestrator.yml create mode 100644 .github/workflows/deploy-windows.yml create mode 100644 .github/workflows/job-cleanup-deployment.yml create mode 100644 .github/workflows/job-deploy-windows.yml create mode 100644 .github/workflows/job-deploy.yml create mode 100644 .github/workflows/job-docker-build.yml create mode 100644 .github/workflows/job-send-notification.yml create mode 100644 .github/workflows/test-automation-v2.yml diff --git a/.github/workflows/deploy-orchestrator.yml b/.github/workflows/deploy-orchestrator.yml new file mode 100644 index 00000000..9e99cec1 --- /dev/null +++ b/.github/workflows/deploy-orchestrator.yml @@ -0,0 +1,138 @@ +name: Deployment orchestrator + +on: + workflow_call: + inputs: + runner_os: + description: 'Runner OS (ubuntu-latest or windows-latest)' + required: true + type: string + azure_location: + description: 'Azure Location For Deployment' + required: false + default: 'australiaeast' + type: string + resource_group_name: + description: 'Resource Group Name (Optional)' + required: false + default: '' + type: string + waf_enabled: + description: 'Enable WAF' + required: false + default: false + type: boolean + EXP: + description: 'Enable EXP' + required: false + default: false + type: boolean + build_docker_image: + description: 'Build And Push Docker Image (Optional)' + required: false + default: false + type: boolean + cleanup_resources: + description: 'Cleanup Deployed Resources' + required: false + default: false + type: boolean + run_e2e_tests: + description: 'Run End-to-End Tests' + required: false + default: 'GoldenPath-Testing' + type: string + AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: + description: 'Log Analytics Workspace ID (Optional)' + required: false + default: '' + type: string + AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: + description: 'AI Project Resource ID (Optional)' + required: false + default: '' + type: string + existing_webapp_url: + description: 'Existing Container WebApp URL (Skips Deployment)' + required: false + default: '' + type: string + trigger_type: + description: 'Trigger type (workflow_dispatch, pull_request, schedule)' + required: true + type: string + +env: + AZURE_DEV_COLLECT_TELEMETRY: ${{ vars.AZURE_DEV_COLLECT_TELEMETRY }} + +jobs: + docker-build: + uses: ./.github/workflows/job-docker-build.yml + with: + trigger_type: ${{ inputs.trigger_type }} + build_docker_image: ${{ inputs.build_docker_image }} + secrets: inherit + + deploy: + if: always() && (inputs.trigger_type != 'workflow_dispatch' || inputs.existing_webapp_url == '' || inputs.existing_webapp_url == null) + needs: docker-build + uses: ./.github/workflows/job-deploy.yml + with: + trigger_type: ${{ inputs.trigger_type }} + runner_os: ${{ inputs.runner_os }} + azure_location: ${{ inputs.azure_location }} + resource_group_name: ${{ inputs.resource_group_name }} + waf_enabled: ${{ inputs.waf_enabled }} + EXP: ${{ inputs.EXP }} + build_docker_image: ${{ inputs.build_docker_image }} + existing_webapp_url: ${{ inputs.existing_webapp_url }} + AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }} + AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }} + docker_image_tag: ${{ needs.docker-build.outputs.IMAGE_TAG }} + run_e2e_tests: ${{ inputs.run_e2e_tests }} + cleanup_resources: ${{ inputs.cleanup_resources }} + secrets: inherit + + e2e-test: + if: always() && ((needs.deploy.result == 'success' && needs.deploy.outputs.WEB_APPURL != '') || (inputs.existing_webapp_url != '' && inputs.existing_webapp_url != null)) && (inputs.trigger_type != 'workflow_dispatch' || (inputs.run_e2e_tests != 'None' && inputs.run_e2e_tests != '' && inputs.run_e2e_tests != null)) + needs: [docker-build, deploy] + uses: ./.github/workflows/test-automation-v2.yml + with: + DOCGEN_URL: ${{ needs.deploy.outputs.WEB_APPURL || inputs.existing_webapp_url }} + TEST_SUITE: ${{ inputs.trigger_type == 'workflow_dispatch' && inputs.run_e2e_tests || 'GoldenPath-Testing' }} + secrets: inherit + + send-notification: + if: always() + needs: [docker-build, deploy, e2e-test] + uses: ./.github/workflows/job-send-notification.yml + with: + trigger_type: ${{ inputs.trigger_type }} + waf_enabled: ${{ inputs.waf_enabled }} + EXP: ${{ inputs.EXP }} + run_e2e_tests: ${{ inputs.run_e2e_tests }} + existing_webapp_url: ${{ inputs.existing_webapp_url }} + deploy_result: ${{ needs.deploy.result }} + e2e_test_result: ${{ needs.e2e-test.result }} + WEB_APPURL: ${{ needs.deploy.outputs.WEB_APPURL || inputs.existing_webapp_url }} + RESOURCE_GROUP_NAME: ${{ needs.deploy.outputs.RESOURCE_GROUP_NAME }} + QUOTA_FAILED: ${{ needs.deploy.outputs.QUOTA_FAILED }} + TEST_SUCCESS: ${{ needs.e2e-test.outputs.TEST_SUCCESS }} + TEST_REPORT_URL: ${{ needs.e2e-test.outputs.TEST_REPORT_URL }} + secrets: inherit + + cleanup-deployment: + if: always() && needs.deploy.result == 'success' && needs.deploy.outputs.RESOURCE_GROUP_NAME != '' && inputs.existing_webapp_url == '' && (inputs.trigger_type != 'workflow_dispatch' || inputs.cleanup_resources) + needs: [docker-build, deploy, e2e-test] + uses: ./.github/workflows/job-cleanup-deployment.yml + with: + runner_os: ${{ inputs.runner_os }} + trigger_type: ${{ inputs.trigger_type }} + cleanup_resources: ${{ inputs.cleanup_resources }} + existing_webapp_url: ${{ inputs.existing_webapp_url }} + RESOURCE_GROUP_NAME: ${{ needs.deploy.outputs.RESOURCE_GROUP_NAME }} + AZURE_LOCATION: ${{ needs.deploy.outputs.AZURE_LOCATION }} + AZURE_ENV_OPENAI_LOCATION: ${{ needs.deploy.outputs.AZURE_ENV_OPENAI_LOCATION }} + ENV_NAME: ${{ needs.deploy.outputs.ENV_NAME }} + IMAGE_TAG: ${{ needs.deploy.outputs.IMAGE_TAG }} + secrets: inherit diff --git a/.github/workflows/deploy-windows.yml b/.github/workflows/deploy-windows.yml new file mode 100644 index 00000000..2e96c838 --- /dev/null +++ b/.github/workflows/deploy-windows.yml @@ -0,0 +1,100 @@ +name: Deploy-Test-Cleanup (v2) Windows +on: + push: + branches: + - main # Adjust this to the branch you want to trigger the deployment on + - dev + - demo + schedule: + - cron: "0 10,22 * * *" # Runs at 10:00 AM and 10:00 PM GMT + + workflow_dispatch: + inputs: + azure_location: + description: 'Azure Location For Deployment' + required: false + default: 'australiaeast' + type: choice + options: + - 'australiaeast' + - 'centralus' + - 'eastasia' + - 'eastus2' + - 'japaneast' + - 'northeurope' + - 'southeastasia' + - 'uksouth' + resource_group_name: + description: 'Resource Group Name (Optional)' + required: false + default: '' + type: string + + waf_enabled: + description: 'Enable WAF' + required: false + default: false + type: boolean + EXP: + description: 'Enable EXP' + required: false + default: false + type: boolean + build_docker_image: + description: 'Build And Push Docker Image (Optional)' + required: false + default: false + type: boolean + + cleanup_resources: + description: 'Cleanup Deployed Resources' + required: false + default: false + type: boolean + + run_e2e_tests: + description: 'Run End-to-End Tests' + required: false + default: 'GoldenPath-Testing' + type: choice + options: + - 'GoldenPath-Testing' + - 'Smoke-Testing' + - 'None' + + AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: + description: 'Log Analytics Workspace ID (Optional)' + required: false + default: '' + type: string + AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: + description: 'AI Project Resource ID (Optional)' + required: false + default: '' + type: string + existing_webapp_url: + description: 'Existing WebApp URL (Skips Deployment)' + required: false + default: '' + type: string + + # schedule: + # - cron: '0 9,21 * * *' # Runs at 9:00 AM and 9:00 PM GMT + +jobs: + Run: + uses: ./.github/workflows/deploy-orchestrator.yml + with: + runner_os: windows-latest + azure_location: ${{ github.event.inputs.azure_location || 'australiaeast' }} + resource_group_name: ${{ github.event.inputs.resource_group_name || '' }} + waf_enabled: ${{ github.event.inputs.waf_enabled == 'true' }} + EXP: ${{ github.event.inputs.EXP == 'true' }} + build_docker_image: ${{ github.event.inputs.build_docker_image == 'true' }} + cleanup_resources: ${{ github.event.inputs.cleanup_resources == 'true' }} + run_e2e_tests: ${{ github.event.inputs.run_e2e_tests || 'GoldenPath-Testing' }} + AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ github.event.inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID || '' }} + AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ github.event.inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID || '' }} + existing_webapp_url: ${{ github.event.inputs.existing_webapp_url || '' }} + trigger_type: ${{ github.event_name }} + secrets: inherit diff --git a/.github/workflows/job-cleanup-deployment.yml b/.github/workflows/job-cleanup-deployment.yml new file mode 100644 index 00000000..6b920a4e --- /dev/null +++ b/.github/workflows/job-cleanup-deployment.yml @@ -0,0 +1,114 @@ +name: Cleanup Deployment Job +on: + workflow_call: + inputs: + runner_os: + description: 'Runner OS (ubuntu-latest or windows-latest)' + required: true + type: string + trigger_type: + description: 'Trigger type (workflow_dispatch, pull_request, schedule)' + required: true + type: string + cleanup_resources: + description: 'Cleanup Deployed Resources' + required: false + default: false + type: boolean + existing_webapp_url: + description: 'Existing Container WebApp URL (Skips Deployment)' + required: false + default: '' + type: string + RESOURCE_GROUP_NAME: + description: 'Resource Group Name to cleanup' + required: true + type: string + AZURE_LOCATION: + description: 'Azure Location' + required: true + type: string + AZURE_ENV_OPENAI_LOCATION: + description: 'Azure OpenAI Location' + required: true + type: string + ENV_NAME: + description: 'Environment Name' + required: true + type: string + IMAGE_TAG: + description: 'Docker Image Tag' + required: true + type: string + +jobs: + cleanup-deployment: + runs-on: ${{ inputs.runner_os }} + continue-on-error: true + env: + RESOURCE_GROUP_NAME: ${{ inputs.RESOURCE_GROUP_NAME }} + AZURE_LOCATION: ${{ inputs.AZURE_LOCATION }} + AZURE_ENV_OPENAI_LOCATION: ${{ inputs.AZURE_ENV_OPENAI_LOCATION }} + ENV_NAME: ${{ inputs.ENV_NAME }} + IMAGE_TAG: ${{ inputs.IMAGE_TAG }} + steps: + - name: Setup Azure CLI + shell: bash + run: | + if [[ "${{ runner.os }}" == "Linux" ]]; then + curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash + fi + az --version + + - name: Login to Azure + shell: bash + run: | + az login --service-principal -u ${{ secrets.AZURE_CLIENT_ID }} -p ${{ secrets.AZURE_CLIENT_SECRET }} --tenant ${{ secrets.AZURE_TENANT_ID }} + az account set --subscription ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Delete Resource Group (Optimized Cleanup) + id: delete_rg + shell: bash + run: | + set -e + echo "🗑️ Starting optimized resource cleanup..." + echo "Deleting resource group: ${{ env.RESOURCE_GROUP_NAME }}" + + az group delete \ + --name "${{ env.RESOURCE_GROUP_NAME }}" \ + --yes \ + --no-wait + + echo "✅ Resource group deletion initiated (running asynchronously)" + echo "Note: Resources will be cleaned up in the background" + + - name: Logout from Azure + if: always() + shell: bash + run: | + azd auth logout || true + az logout || echo "Warning: Failed to logout from Azure CLI" + echo "Logged out from Azure." + + - name: Generate Cleanup Job Summary + if: always() + shell: bash + run: | + echo "## 🧹 Cleanup Job Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| **Resource Group deletion Status** | ${{ steps.delete_rg.outcome == 'success' && '✅ Initiated' || '❌ Failed' }} |" >> $GITHUB_STEP_SUMMARY + echo "| **Resource Group** | \`${{ env.RESOURCE_GROUP_NAME }}\` |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + if [[ "${{ steps.delete_rg.outcome }}" == "success" ]]; then + echo "### ✅ Cleanup Details" >> $GITHUB_STEP_SUMMARY + echo "- Successfully initiated deletion for Resource Group \`${{ env.RESOURCE_GROUP_NAME }}\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + else + echo "### ❌ Cleanup Failed" >> $GITHUB_STEP_SUMMARY + echo "- Cleanup process encountered an error" >> $GITHUB_STEP_SUMMARY + echo "- Manual cleanup may be required for:" >> $GITHUB_STEP_SUMMARY + echo " - Resource Group: \`${{ env.RESOURCE_GROUP_NAME }}\`" >> $GITHUB_STEP_SUMMARY + echo "- Check the cleanup-deployment job logs for detailed error information" >> $GITHUB_STEP_SUMMARY + fi diff --git a/.github/workflows/job-deploy-windows.yml b/.github/workflows/job-deploy-windows.yml new file mode 100644 index 00000000..09af1ad5 --- /dev/null +++ b/.github/workflows/job-deploy-windows.yml @@ -0,0 +1,262 @@ +name: Deploy Steps - Windows + +on: + workflow_call: + inputs: + ENV_NAME: + required: true + type: string + AZURE_ENV_OPENAI_LOCATION: + required: true + type: string + AZURE_LOCATION: + required: true + type: string + RESOURCE_GROUP_NAME: + required: true + type: string + IMAGE_TAG: + required: true + type: string + BUILD_DOCKER_IMAGE: + required: true + type: string + EXP: + required: true + type: string + WAF_ENABLED: + required: false + type: string + default: 'false' + AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: + required: false + type: string + AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: + required: false + type: string + outputs: + WEB_APPURL: + description: "Container Web App URL" + value: ${{ jobs.deploy-windows.outputs.WEB_APPURL }} + +jobs: + deploy-windows: + runs-on: windows-latest + env: + AZURE_DEV_COLLECT_TELEMETRY: ${{ vars.AZURE_DEV_COLLECT_TELEMETRY }} + outputs: + WEB_APPURL: ${{ steps.get_output_windows.outputs.WEB_APPURL }} + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Install Kubernetes CLI (kubectl) + shell: bash + run: | + az aks install-cli + az extension add --name aks-preview + + - name: Install Helm + shell: bash + run: | + # If helm is already available on the runner, print version and skip installation + if command -v helm >/dev/null 2>&1; then + echo "helm already installed: $(helm version --short 2>/dev/null || true)" + exit 0 + fi + + # Ensure prerequisites are present + sudo apt-get update + sudo apt-get install -y apt-transport-https ca-certificates curl gnupg lsb-release + + # Ensure keyrings dir exists + sudo mkdir -p /usr/share/keyrings + + # Add Helm GPG key (use -fS to fail fast on curl errors) + curl -fsSL https://baltocdn.com/helm/signing.asc | gpg --dearmor | sudo tee /usr/share/keyrings/helm.gpg >/dev/null + + # Add the Helm apt repository + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/helm.gpg] https://baltocdn.com/helm/stable/debian/ all main" | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list + + # Install helm + sudo apt-get update + sudo apt-get install -y helm + + # Verify + echo "Installed helm version:" + helm version + + - name: Set up Docker + uses: docker/setup-buildx-action@v3 + with: + driver: docker + + - name: Configure Parameters Based on WAF Setting + shell: bash + run: | + if [[ "${{ inputs.WAF_ENABLED }}" == "true" ]]; then + cp infra/main.waf.parameters.json infra/main.parameters.json + echo "✅ Successfully copied WAF parameters to main parameters file" + else + echo "🔧 Configuring Non-WAF deployment - using default main.parameters.json..." + fi + + - name: Setup Azure Developer CLI (Windows) + uses: Azure/setup-azd@v2 + + - name: Login to AZD + id: login-azure + shell: bash + run: | + az login --service-principal -u ${{ secrets.AZURE_CLIENT_ID }} -p ${{ secrets.AZURE_CLIENT_SECRET }} --tenant ${{ secrets.AZURE_TENANT_ID }} + az account set --subscription ${{ secrets.AZURE_SUBSCRIPTION_ID }} + azd auth login --client-id ${{ secrets.AZURE_CLIENT_ID }} --client-secret ${{ secrets.AZURE_CLIENT_SECRET }} --tenant-id ${{ secrets.AZURE_TENANT_ID }} + + + - name: Deploy using azd up and extract values (Windows) + id: get_output_windows + shell: pwsh + run: | + $ErrorActionPreference = "Stop" + + Write-Host "Creating environment..." + azd env new ${{ inputs.ENV_NAME }} --no-prompt + Write-Host "Environment created: ${{ inputs.ENV_NAME }}" + + Write-Host "Setting default subscription..." + azd config set defaults.subscription ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + # Set additional parameters + azd env set AZURE_SUBSCRIPTION_ID="${{ secrets.AZURE_SUBSCRIPTION_ID }}" + azd env set AZURE_ENV_OPENAI_LOCATION="${{ inputs.AZURE_ENV_OPENAI_LOCATION }}" + azd env set AZURE_LOCATION="${{ inputs.AZURE_LOCATION }}" + azd env set AZURE_RESOURCE_GROUP="${{ inputs.RESOURCE_GROUP_NAME }}" + azd env set AZURE_ENV_IMAGETAG="${{ inputs.IMAGE_TAG }}" + + # Set ACR name only when building Docker image + if ("${{ inputs.BUILD_DOCKER_IMAGE }}" -eq "true") { + $ACR_NAME = "${{ secrets.ACR_TEST_USERNAME }}" + azd env set AZURE_ENV_ACR_NAME="$ACR_NAME" + Write-Host "Set ACR name to: $ACR_NAME" + } else { + Write-Host "Skipping ACR name configuration (using existing image)" + } + + if ("${{ inputs.EXP }}" -eq "true") { + Write-Host "✅ EXP ENABLED - Setting EXP parameters..." + + # Set EXP variables dynamically + if ("${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}" -ne "") { + $EXP_LOG_ANALYTICS_ID = "${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}" + } else { + $EXP_LOG_ANALYTICS_ID = "${{ secrets.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}" + } + + if ("${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }}" -ne "") { + $EXP_AI_PROJECT_ID = "${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }}" + } else { + $EXP_AI_PROJECT_ID = "${{ secrets.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }}" + } + + Write-Host "AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: $EXP_LOG_ANALYTICS_ID" + Write-Host "AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: $EXP_AI_PROJECT_ID" + azd env set AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID="$EXP_LOG_ANALYTICS_ID" + azd env set AZURE_EXISTING_AI_PROJECT_RESOURCE_ID="$EXP_AI_PROJECT_ID" + } else { + Write-Host "❌ EXP DISABLED - Skipping EXP parameters" + } + + # Deploy using azd up + azd up --no-prompt + + Write-Host "✅ Deployment succeeded." + + # Get deployment outputs using azd + Write-Host "Extracting deployment outputs..." + $DEPLOY_OUTPUT = azd env get-values --output json | ConvertFrom-Json + Write-Host "Deployment output: $($DEPLOY_OUTPUT | ConvertTo-Json -Depth 10)" + + if (-not $DEPLOY_OUTPUT) { + Write-Host "Error: Deployment output is empty. Please check the deployment logs." + exit 1 + } + + + $AI_FOUNDRY_RESOURCE_ID = $DEPLOY_OUTPUT.AI_FOUNDRY_RESOURCE_ID + "AI_FOUNDRY_RESOURCE_ID=$AI_FOUNDRY_RESOURCE_ID" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + + $AI_SEARCH_SERVICE_NAME = $DEPLOY_OUTPUT.AI_SEARCH_SERVICE_NAME + "AI_SEARCH_SERVICE_NAME=$AI_SEARCH_SERVICE_NAME" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + + $AZURE_COSMOSDB_ACCOUNT = $DEPLOY_OUTPUT.AZURE_COSMOSDB_ACCOUNT + "AZURE_COSMOSDB_ACCOUNT=$AZURE_COSMOSDB_ACCOUNT" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + + $STORAGE_ACCOUNT_NAME = $DEPLOY_OUTPUT.STORAGE_ACCOUNT_NAME + "STORAGE_ACCOUNT_NAME=$STORAGE_ACCOUNT_NAME" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + + $STORAGE_CONTAINER_NAME = $DEPLOY_OUTPUT.STORAGE_CONTAINER_NAME + "STORAGE_CONTAINER_NAME=$STORAGE_CONTAINER_NAME" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + + $KEY_VAULT_NAME = $DEPLOY_OUTPUT.KEY_VAULT_NAME + "KEY_VAULT_NAME=$KEY_VAULT_NAME" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + + $RESOURCE_GROUP_NAME = $DEPLOY_OUTPUT.RESOURCE_GROUP_NAME + "RESOURCE_GROUP_NAME=$RESOURCE_GROUP_NAME" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + + $WEB_APP_URL = $DEPLOY_OUTPUT.WEB_APP_URL + "WEB_APPURL=$WEB_APP_URL" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "WEB_APPURL=$WEB_APP_URL" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + + - name: Run Post-Deployment Script + id: post_deploy + env: + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + shell: bash + run: | + set -e + az account set --subscription "${{ secrets.AZURE_SUBSCRIPTION_ID }}" + + echo "Running post-deployment script..." + + bash ./infra/scripts/process_sample_data.sh \ + "${{ env.STORAGE_ACCOUNT_NAME }}" \ + "${{ env.STORAGE_CONTAINER_NAME }}" \ + "${{ env.KEY_VAULT_NAME }}" \ + "${{ env.AZURE_COSMOSDB_ACCOUNT }}" \ + "${{ env.RESOURCE_GROUP_NAME }}" \ + "${{ env.AI_SEARCH_SERVICE_NAME }}" \ + "${{ secrets.AZURE_CLIENT_ID }}" \ + "${{ env.AI_FOUNDRY_RESOURCE_ID }}" + + - name: Generate Deploy Job Summary + if: always() + shell: bash + run: | + echo "## 🚀 Deploy Job Summary (Windows)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| **Job Status** | ${{ job.status == 'success' && '✅ Success' || '❌ Failed' }} |" >> $GITHUB_STEP_SUMMARY + echo "| **Resource Group** | \`${{ inputs.RESOURCE_GROUP_NAME }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Configuration Type** | \`${{ inputs.WAF_ENABLED == 'true' && inputs.EXP == 'true' && 'WAF + EXP' || inputs.WAF_ENABLED == 'true' && inputs.EXP != 'true' && 'WAF + Non-EXP' || inputs.WAF_ENABLED != 'true' && inputs.EXP == 'true' && 'Non-WAF + EXP' || 'Non-WAF + Non-EXP' }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Azure Region (Infrastructure)** | \`${{ inputs.AZURE_LOCATION }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Azure OpenAI Region** | \`${{ inputs.AZURE_ENV_OPENAI_LOCATION }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Docker Image Tag** | \`${{ inputs.IMAGE_TAG }}\` |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + if [[ "${{ job.status }}" == "success" ]]; then + echo "### ✅ Deployment Details" >> $GITHUB_STEP_SUMMARY + echo "- **Web App URL**: [${{ steps.get_output_windows.outputs.WEB_APPURL }}](${{ steps.get_output_windows.outputs.WEB_APPURL }})" >> $GITHUB_STEP_SUMMARY + echo "- Successfully deployed to Azure with all resources configured" >> $GITHUB_STEP_SUMMARY + echo "- Post-deployment scripts executed successfully" >> $GITHUB_STEP_SUMMARY + else + echo "### ❌ Deployment Failed" >> $GITHUB_STEP_SUMMARY + echo "- Deployment process encountered an error" >> $GITHUB_STEP_SUMMARY + echo "- Check the deploy job for detailed error information" >> $GITHUB_STEP_SUMMARY + fi + + - name: Logout from Azure + if: always() + shell: bash + run: | + az logout || true + echo "Logged out from Azure." diff --git a/.github/workflows/job-deploy.yml b/.github/workflows/job-deploy.yml new file mode 100644 index 00000000..fc77e6eb --- /dev/null +++ b/.github/workflows/job-deploy.yml @@ -0,0 +1,362 @@ +name: Deploy Job + +on: + workflow_call: + inputs: + trigger_type: + description: 'Trigger type (workflow_dispatch, pull_request, schedule)' + required: true + type: string + runner_os: + description: 'Runner OS (ubuntu-latest or windows-latest)' + required: true + type: string + azure_location: + description: 'Azure Location For Deployment' + required: false + default: 'australiaeast' + type: string + resource_group_name: + description: 'Resource Group Name (Optional)' + required: false + default: '' + type: string + waf_enabled: + description: 'Enable WAF' + required: false + default: false + type: boolean + EXP: + description: 'Enable EXP' + required: false + default: false + type: boolean + build_docker_image: + description: 'Build And Push Docker Image (Optional)' + required: false + default: false + type: boolean + cleanup_resources: + description: 'Cleanup Deployed Resources' + required: false + default: false + type: boolean + run_e2e_tests: + description: 'Run End-to-End Tests' + required: false + default: 'GoldenPath-Testing' + type: string + existing_webapp_url: + description: 'Existing Container WebApp URL (Skips Deployment)' + required: false + default: '' + type: string + AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: + description: 'Log Analytics Workspace ID (Optional)' + required: false + default: '' + type: string + AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: + description: 'AI Project Resource ID (Optional)' + required: false + default: '' + type: string + docker_image_tag: + description: 'Docker Image Tag from build job' + required: false + default: '' + type: string + outputs: + RESOURCE_GROUP_NAME: + description: "Resource Group Name" + value: ${{ jobs.azure-setup.outputs.RESOURCE_GROUP_NAME }} + WEB_APPURL: + description: "Container Web App URL" + value: ${{ jobs.deploy-linux.outputs.WEB_APPURL || jobs.deploy-windows.outputs.WEB_APPURL }} + ENV_NAME: + description: "Environment Name" + value: ${{ jobs.azure-setup.outputs.ENV_NAME }} + AZURE_LOCATION: + description: "Azure Location" + value: ${{ jobs.azure-setup.outputs.AZURE_LOCATION }} + AZURE_ENV_OPENAI_LOCATION: + description: "Azure OpenAI Location" + value: ${{ jobs.azure-setup.outputs.AZURE_ENV_OPENAI_LOCATION }} + IMAGE_TAG: + description: "Docker Image Tag Used" + value: ${{ jobs.azure-setup.outputs.IMAGE_TAG }} + QUOTA_FAILED: + description: "Quota Check Failed Flag" + value: ${{ jobs.azure-setup.outputs.QUOTA_FAILED }} + +env: + GPT_MIN_CAPACITY: 150 + TEXT_EMBEDDING_MIN_CAPACITY: 80 + BRANCH_NAME: ${{ github.event.workflow_run.head_branch || github.head_ref || github.ref_name }} + WAF_ENABLED: ${{ inputs.trigger_type == 'workflow_dispatch' && (inputs.waf_enabled || false) || false }} + EXP: ${{ inputs.trigger_type == 'workflow_dispatch' && (inputs.EXP || false) || false }} + CLEANUP_RESOURCES: ${{ inputs.trigger_type != 'workflow_dispatch' || inputs.cleanup_resources }} + RUN_E2E_TESTS: ${{ inputs.trigger_type == 'workflow_dispatch' && (inputs.run_e2e_tests || 'GoldenPath-Testing') || 'GoldenPath-Testing' }} + BUILD_DOCKER_IMAGE: ${{ inputs.trigger_type == 'workflow_dispatch' && (inputs.build_docker_image || false) || false }} + +jobs: + azure-setup: + name: Azure Setup + if: inputs.trigger_type != 'workflow_dispatch' || inputs.existing_webapp_url == '' || inputs.existing_webapp_url == null + runs-on: ubuntu-latest + outputs: + RESOURCE_GROUP_NAME: ${{ steps.check_create_rg.outputs.RESOURCE_GROUP_NAME }} + ENV_NAME: ${{ steps.generate_env_name.outputs.ENV_NAME }} + AZURE_LOCATION: ${{ steps.set_region.outputs.AZURE_LOCATION }} + AZURE_ENV_OPENAI_LOCATION: ${{ steps.set_region.outputs.AZURE_ENV_OPENAI_LOCATION }} + IMAGE_TAG: ${{ steps.determine_image_tag.outputs.IMAGE_TAG }} + QUOTA_FAILED: ${{ steps.quota_failure_output.outputs.QUOTA_FAILED }} + + steps: + - name: Validate and Auto-Configure EXP + shell: bash + run: | + echo "🔍 Validating EXP configuration..." + + if [[ "${{ inputs.EXP }}" != "true" ]]; then + if [[ -n "${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}" ]] || [[ -n "${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }}" ]]; then + echo "🔧 AUTO-ENABLING EXP: EXP parameter values were provided but EXP was not explicitly enabled." + echo "" + echo "You provided values for:" + [[ -n "${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}" ]] && echo " - Azure Log Analytics Workspace ID: '${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}'" + [[ -n "${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }}" ]] && echo " - Azure AI Project Resource ID: '${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }}'" + echo "" + echo "✅ Automatically enabling EXP to use these values." + echo "EXP=true" >> $GITHUB_ENV + echo "📌 EXP has been automatically enabled for this deployment." + fi + fi + + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Login to Azure + shell: bash + run: | + az login --service-principal -u ${{ secrets.AZURE_CLIENT_ID }} -p ${{ secrets.AZURE_CLIENT_SECRET }} --tenant ${{ secrets.AZURE_TENANT_ID }} + az account set --subscription ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Run Quota Check + id: quota-check + run: | + export AZURE_CLIENT_ID=${{ secrets.AZURE_CLIENT_ID }} + export AZURE_TENANT_ID=${{ secrets.AZURE_TENANT_ID }} + export AZURE_CLIENT_SECRET=${{ secrets.AZURE_CLIENT_SECRET }} + export AZURE_SUBSCRIPTION_ID="${{ secrets.AZURE_SUBSCRIPTION_ID }}" + export GPT_MIN_CAPACITY=${{ env.GPT_MIN_CAPACITY }} + export TEXT_EMBEDDING_MIN_CAPACITY=${{ env.TEXT_EMBEDDING_MIN_CAPACITY }} + export AZURE_REGIONS="${{ vars.AZURE_REGIONS }}" + + chmod +x scripts/checkquota.sh + if ! scripts/checkquota.sh; then + if grep -q "No region with sufficient quota found" scripts/checkquota.sh; then + echo "QUOTA_FAILED=true" >> $GITHUB_ENV + fi + exit 1 + fi + + - name: Set Quota Failure Output + id: quota_failure_output + if: env.QUOTA_FAILED == 'true' + shell: bash + run: | + echo "QUOTA_FAILED=true" >> $GITHUB_OUTPUT + echo "Quota check failed - will notify via separate notification job" + + - name: Fail Pipeline if Quota Check Fails + if: env.QUOTA_FAILED == 'true' + shell: bash + run: exit 1 + + - name: Set Deployment Region + id: set_region + shell: bash + run: | + echo "Selected Region from Quota Check: $VALID_REGION" + echo "AZURE_ENV_OPENAI_LOCATION=$VALID_REGION" >> $GITHUB_ENV + echo "AZURE_ENV_OPENAI_LOCATION=$VALID_REGION" >> $GITHUB_OUTPUT + + if [[ "${{ inputs.trigger_type }}" == "workflow_dispatch" && -n "${{ inputs.azure_location }}" ]]; then + USER_SELECTED_LOCATION="${{ inputs.azure_location }}" + echo "Using user-selected Azure location: $USER_SELECTED_LOCATION" + echo "AZURE_LOCATION=$USER_SELECTED_LOCATION" >> $GITHUB_ENV + echo "AZURE_LOCATION=$USER_SELECTED_LOCATION" >> $GITHUB_OUTPUT + else + echo "Using location from quota check for automatic triggers: $VALID_REGION" + echo "AZURE_LOCATION=$VALID_REGION" >> $GITHUB_ENV + echo "AZURE_LOCATION=$VALID_REGION" >> $GITHUB_OUTPUT + fi + + - name: Generate Resource Group Name + id: generate_rg_name + shell: bash + run: | + # Check if a resource group name was provided as input + if [[ -n "${{ inputs.resource_group_name }}" ]]; then + echo "Using provided Resource Group name: ${{ inputs.resource_group_name }}" + echo "RESOURCE_GROUP_NAME=${{ inputs.resource_group_name }}" >> $GITHUB_ENV + else + echo "Generating a unique resource group name..." + ACCL_NAME="docgenv2" # Account name as specified + SHORT_UUID=$(uuidgen | cut -d'-' -f1) + UNIQUE_RG_NAME="arg-${ACCL_NAME}-${SHORT_UUID}" + echo "RESOURCE_GROUP_NAME=${UNIQUE_RG_NAME}" >> $GITHUB_ENV + echo "Generated RESOURCE_GROUP_NAME: ${UNIQUE_RG_NAME}" + fi + + - name: Install Bicep CLI + shell: bash + run: az bicep install + + - name: Check and Create Resource Group + id: check_create_rg + shell: bash + run: | + set -e + echo "🔍 Checking if resource group '$RESOURCE_GROUP_NAME' exists..." + rg_exists=$(az group exists --name $RESOURCE_GROUP_NAME) + if [ "$rg_exists" = "false" ]; then + echo "📦 Resource group does not exist. Creating new resource group '$RESOURCE_GROUP_NAME' in location '$AZURE_LOCATION'..." + az group create --name $RESOURCE_GROUP_NAME --location $AZURE_LOCATION || { echo "❌ Error creating resource group"; exit 1; } + echo "✅ Resource group '$RESOURCE_GROUP_NAME' created successfully." + else + echo "✅ Resource group '$RESOURCE_GROUP_NAME' already exists. Deploying to existing resource group." + fi + echo "RESOURCE_GROUP_NAME=$RESOURCE_GROUP_NAME" >> $GITHUB_OUTPUT + echo "RESOURCE_GROUP_NAME=$RESOURCE_GROUP_NAME" >> $GITHUB_ENV + + - name: Generate Unique Solution Prefix + id: generate_solution_prefix + shell: bash + run: | + set -e + COMMON_PART="psldg" + TIMESTAMP=$(date +%s) + UPDATED_TIMESTAMP=$(echo $TIMESTAMP | tail -c 6) + UNIQUE_SOLUTION_PREFIX="${COMMON_PART}${UPDATED_TIMESTAMP}" + echo "SOLUTION_PREFIX=${UNIQUE_SOLUTION_PREFIX}" >> $GITHUB_ENV + echo "Generated SOLUTION_PREFIX: ${UNIQUE_SOLUTION_PREFIX}" + + - name: Determine Docker Image Tag + id: determine_image_tag + run: | + if [[ "${{ env.BUILD_DOCKER_IMAGE }}" == "true" ]]; then + if [[ -n "${{ inputs.docker_image_tag }}" ]]; then + IMAGE_TAG="${{ inputs.docker_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 }}" + echo "Current branch: $BRANCH_NAME" + + # Determine image tag based on branch + if [[ "$BRANCH_NAME" == "main" ]]; then + IMAGE_TAG="latest_waf" + echo "Using main branch - image tag: latest_waf" + elif [[ "$BRANCH_NAME" == "dev" ]]; then + IMAGE_TAG="dev" + echo "Using dev branch - image tag: dev" + elif [[ "$BRANCH_NAME" == "demo" ]]; then + IMAGE_TAG="demo" + echo "Using demo branch - image tag: demo" + else + IMAGE_TAG="latest_waf" + echo "Using default for branch '$BRANCH_NAME' - image tag: latest_waf" + fi + + echo "Using existing Docker image tag: $IMAGE_TAG" + fi + + echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV + echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_OUTPUT + + - name: Generate Unique Environment Name + id: generate_env_name + shell: bash + run: | + COMMON_PART="pslc" + TIMESTAMP=$(date +%s) + UPDATED_TIMESTAMP=$(echo $TIMESTAMP | tail -c 6) + UNIQUE_ENV_NAME="${COMMON_PART}${UPDATED_TIMESTAMP}" + echo "ENV_NAME=${UNIQUE_ENV_NAME}" >> $GITHUB_ENV + echo "Generated Environment Name: ${UNIQUE_ENV_NAME}" + echo "ENV_NAME=${UNIQUE_ENV_NAME}" >> $GITHUB_OUTPUT + + - name: Display Workflow Configuration to GitHub Summary + shell: bash + run: | + echo "## 📋 Workflow Configuration Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Configuration | Value |" >> $GITHUB_STEP_SUMMARY + echo "|---------------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| **Trigger Type** | \`${{ github.event_name }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Branch** | \`${{ env.BRANCH_NAME }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Runner OS** | \`${{ inputs.runner_os }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **WAF Enabled** | ${{ env.WAF_ENABLED == 'true' && '✅ Yes' || '❌ No' }} |" >> $GITHUB_STEP_SUMMARY + echo "| **EXP Enabled** | ${{ env.EXP == 'true' && '✅ Yes' || '❌ No' }} |" >> $GITHUB_STEP_SUMMARY + echo "| **Run E2E Tests** | \`${{ env.RUN_E2E_TESTS }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Cleanup Resources** | ${{ env.CLEANUP_RESOURCES == 'true' && '✅ Yes' || '❌ No' }} |" >> $GITHUB_STEP_SUMMARY + echo "| **Build Docker Image** | ${{ env.BUILD_DOCKER_IMAGE == 'true' && '✅ Yes' || '❌ No' }} |" >> $GITHUB_STEP_SUMMARY + + if [[ "${{ inputs.trigger_type }}" == "workflow_dispatch" && -n "${{ inputs.azure_location }}" ]]; then + echo "| **Azure Location** | \`${{ inputs.azure_location }}\` (User Selected) |" >> $GITHUB_STEP_SUMMARY + fi + + if [[ -n "${{ inputs.resource_group_name }}" ]]; then + echo "| **Resource Group** | \`${{ inputs.resource_group_name }}\` (Pre-specified) |" >> $GITHUB_STEP_SUMMARY + else + echo "| **Resource Group** | \`${{ env.RESOURCE_GROUP_NAME }}\` (Auto-generated) |" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + + if [[ "${{ inputs.trigger_type }}" != "workflow_dispatch" ]]; then + echo "ℹ️ **Note:** Automatic Trigger - Using Non-WAF + Non-EXP configuration" >> $GITHUB_STEP_SUMMARY + else + echo "ℹ️ **Note:** Manual Trigger - Using user-specified configuration" >> $GITHUB_STEP_SUMMARY + fi + + deploy-linux: + name: Deploy on Linux + needs: azure-setup + if: inputs.runner_os == 'ubuntu-latest' && always() && needs.azure-setup.result == 'success' + uses: ./.github/workflows/job-deploy-linux.yml + with: + ENV_NAME: ${{ needs.azure-setup.outputs.ENV_NAME }} + AZURE_ENV_OPENAI_LOCATION: ${{ needs.azure-setup.outputs.AZURE_ENV_OPENAI_LOCATION }} + AZURE_LOCATION: ${{ needs.azure-setup.outputs.AZURE_LOCATION }} + RESOURCE_GROUP_NAME: ${{ needs.azure-setup.outputs.RESOURCE_GROUP_NAME }} + IMAGE_TAG: ${{ needs.azure-setup.outputs.IMAGE_TAG }} + BUILD_DOCKER_IMAGE: ${{ inputs.build_docker_image || 'false' }} + EXP: ${{ inputs.EXP || 'false' }} + WAF_ENABLED: ${{ inputs.waf_enabled == true && 'true' || 'false' }} + AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }} + AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }} + secrets: inherit + + deploy-windows: + name: Deploy on Windows + needs: azure-setup + if: inputs.runner_os == 'windows-latest' && always() && needs.azure-setup.result == 'success' + uses: ./.github/workflows/job-deploy-windows.yml + with: + ENV_NAME: ${{ needs.azure-setup.outputs.ENV_NAME }} + AZURE_ENV_OPENAI_LOCATION: ${{ needs.azure-setup.outputs.AZURE_ENV_OPENAI_LOCATION }} + AZURE_LOCATION: ${{ needs.azure-setup.outputs.AZURE_LOCATION }} + RESOURCE_GROUP_NAME: ${{ needs.azure-setup.outputs.RESOURCE_GROUP_NAME }} + IMAGE_TAG: ${{ needs.azure-setup.outputs.IMAGE_TAG }} + BUILD_DOCKER_IMAGE: ${{ inputs.build_docker_image || 'false' }} + EXP: ${{ inputs.EXP || 'false' }} + WAF_ENABLED: ${{ inputs.waf_enabled == true && 'true' || 'false' }} + AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }} + AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }} + secrets: inherit diff --git a/.github/workflows/job-docker-build.yml b/.github/workflows/job-docker-build.yml new file mode 100644 index 00000000..62956a43 --- /dev/null +++ b/.github/workflows/job-docker-build.yml @@ -0,0 +1,99 @@ +name: Docker Build Job + +on: + workflow_call: + inputs: + trigger_type: + description: 'Trigger type (workflow_dispatch, pull_request, schedule)' + required: true + type: string + build_docker_image: + description: 'Build And Push Docker Image (Optional)' + required: false + default: false + type: boolean + outputs: + IMAGE_TAG: + description: "Generated Docker Image Tag" + value: ${{ jobs.docker-build.outputs.IMAGE_TAG }} + +env: + BRANCH_NAME: ${{ github.event.workflow_run.head_branch || github.head_ref || github.ref_name }} + +jobs: + docker-build: + if: inputs.trigger_type == 'workflow_dispatch' && 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 + shell: bash + run: | + echo "🔨 Building new Docker image - generating unique tag..." + TIMESTAMP=$(date +%Y%m%d-%H%M%S) + RUN_ID="${{ github.run_id }}" + BRANCH_NAME="${{ github.head_ref || github.ref_name }}" + 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 + env: + DOCKER_BUILD_SUMMARY: false + 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 + shell: bash + run: | + echo "✅ Docker image successfully built and pushed" + echo "Image tag: ${{ steps.generate_docker_tag.outputs.IMAGE_TAG }}" + + - name: Generate Docker Build Summary + if: always() + shell: bash + run: | + ACR_NAME=$(echo "${{ secrets.ACR_TEST_LOGIN_SERVER }}") + echo "## 🐳 Docker Build Job Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| **Job Status** | ${{ job.status == 'success' && '✅ Success' || '❌ Failed' }} |" >> $GITHUB_STEP_SUMMARY + echo "| **Image Tag** | \`${{ steps.generate_docker_tag.outputs.IMAGE_TAG }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Branch** | ${{ env.BRANCH_NAME }} |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + if [[ "${{ job.status }}" == "success" ]]; then + echo "### ✅ Build Details" >> $GITHUB_STEP_SUMMARY + echo "Successfully built and pushed one Docker image to ACR:" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Built Images:**" >> $GITHUB_STEP_SUMMARY + echo "- \`${ACR_NAME}.azurecr.io/webapp:${{ steps.generate_docker_tag.outputs.IMAGE_TAG }}\`" >> $GITHUB_STEP_SUMMARY + else + echo "### ❌ Build Failed" >> $GITHUB_STEP_SUMMARY + echo "- Docker build process encountered an error" >> $GITHUB_STEP_SUMMARY + echo "- Check the docker-build job for detailed error information" >> $GITHUB_STEP_SUMMARY + fi diff --git a/.github/workflows/job-send-notification.yml b/.github/workflows/job-send-notification.yml new file mode 100644 index 00000000..87baad34 --- /dev/null +++ b/.github/workflows/job-send-notification.yml @@ -0,0 +1,224 @@ +name: Send Notification Job + +on: + workflow_call: + inputs: + trigger_type: + description: 'Trigger type (workflow_dispatch, pull_request, schedule)' + required: true + type: string + waf_enabled: + description: 'Enable WAF' + required: false + default: false + type: boolean + EXP: + description: 'Enable EXP' + required: false + default: false + type: boolean + run_e2e_tests: + description: 'Run End-to-End Tests' + required: false + default: 'GoldenPath-Testing' + type: string + existing_webapp_url: + description: 'Existing Container WebApp URL (Skips Deployment)' + required: false + default: '' + type: string + deploy_result: + description: 'Deploy job result (success, failure, skipped)' + required: true + type: string + e2e_test_result: + description: 'E2E test job result (success, failure, skipped)' + required: true + type: string + WEB_APPURL: + description: 'Container Web App URL' + required: false + default: '' + type: string + RESOURCE_GROUP_NAME: + description: 'Resource Group Name' + required: false + default: '' + type: string + QUOTA_FAILED: + description: 'Quota Check Failed Flag' + required: false + default: 'false' + type: string + TEST_SUCCESS: + description: 'Test Success Flag' + required: false + default: '' + type: string + TEST_REPORT_URL: + description: 'Test Report URL' + required: false + default: '' + type: string + +env: + GPT_MIN_CAPACITY: 100 + BRANCH_NAME: ${{ github.event.workflow_run.head_branch || github.head_ref || github.ref_name }} + WAF_ENABLED: ${{ inputs.trigger_type == 'workflow_dispatch' && (inputs.waf_enabled || false) || false }} + EXP: ${{ inputs.trigger_type == 'workflow_dispatch' && (inputs.EXP || false) || false }} + RUN_E2E_TESTS: ${{ inputs.trigger_type == 'workflow_dispatch' && (inputs.run_e2e_tests || 'GoldenPath-Testing') || 'GoldenPath-Testing' }} + +jobs: + send-notification: + runs-on: ubuntu-latest + continue-on-error: true + env: + accelerator_name: "DocGen" + steps: + - name: Determine Test Suite Display Name + id: test_suite + shell: bash + run: | + if [ "${{ env.RUN_E2E_TESTS }}" = "GoldenPath-Testing" ]; then + TEST_SUITE_NAME="Golden Path Testing" + elif [ "${{ env.RUN_E2E_TESTS }}" = "Smoke-Testing" ]; then + TEST_SUITE_NAME="Smoke Testing" + elif [ "${{ env.RUN_E2E_TESTS }}" = "None" ]; then + TEST_SUITE_NAME="None" + else + TEST_SUITE_NAME="${{ env.RUN_E2E_TESTS }}" + fi + echo "TEST_SUITE_NAME=$TEST_SUITE_NAME" >> $GITHUB_OUTPUT + echo "Test Suite: $TEST_SUITE_NAME" + + - name: Send Quota Failure Notification + if: inputs.deploy_result == 'failure' && inputs.QUOTA_FAILED == 'true' + shell: bash + run: | + RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + EMAIL_BODY=$(cat <Dear Team,

We would like to inform you that the ${{ env.accelerator_name }} deployment has failed due to insufficient quota in the requested regions.

Issue Details:
• Quota check failed for GPT model
• Required GPT Capacity: ${{ env.GPT_MIN_CAPACITY }}
• Checked Regions: ${{ vars.AZURE_REGIONS }}

Run URL: ${RUN_URL}

Please resolve the quota issue and retry the deployment.

Best regards,
Your Automation Team

", + "subject": "${{ env.accelerator_name }} Pipeline - Failed (Insufficient Quota)" + } + EOF + ) + + curl -X POST "${{ secrets.EMAILNOTIFICATION_LOGICAPP_URL_TA }}" \ + -H "Content-Type: application/json" \ + -d "$EMAIL_BODY" || echo "Failed to send quota failure notification" + + - name: Send Deployment Failure Notification + if: inputs.deploy_result == 'failure' && inputs.QUOTA_FAILED != 'true' + shell: bash + run: | + RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + RESOURCE_GROUP="${{ inputs.RESOURCE_GROUP_NAME }}" + + EMAIL_BODY=$(cat <Dear Team,

We would like to inform you that the ${{ env.accelerator_name }} deployment process has encountered an issue and has failed to complete successfully.

Deployment Details:
• Resource Group: ${RESOURCE_GROUP}
• WAF Enabled: ${{ env.WAF_ENABLED }}
• EXP Enabled: ${{ env.EXP }}

Run URL: ${RUN_URL}

Please investigate the deployment failure at your earliest convenience.

Best regards,
Your Automation Team

", + "subject": "${{ env.accelerator_name }} Pipeline - Failed" + } + EOF + ) + + curl -X POST "${{ secrets.EMAILNOTIFICATION_LOGICAPP_URL_TA }}" \ + -H "Content-Type: application/json" \ + -d "$EMAIL_BODY" || echo "Failed to send deployment failure notification" + + - name: Send Success Notification + if: inputs.deploy_result == 'success' && (inputs.e2e_test_result == 'skipped' || inputs.TEST_SUCCESS == 'true') + shell: bash + run: | + RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + WEBAPP_URL="${{ inputs.WEB_APPURL || inputs.existing_webapp_url }}" + RESOURCE_GROUP="${{ inputs.RESOURCE_GROUP_NAME }}" + TEST_REPORT_URL="${{ inputs.TEST_REPORT_URL }}" + TEST_SUITE_NAME="${{ steps.test_suite.outputs.TEST_SUITE_NAME }}" + + if [ "${{ inputs.e2e_test_result }}" = "skipped" ]; then + EMAIL_BODY=$(cat <Dear Team,

We would like to inform you that the ${{ env.accelerator_name }} deployment has completed successfully.

Deployment Details:
• Resource Group: ${RESOURCE_GROUP}
• Web App URL: ${WEBAPP_URL}
• E2E Tests: Skipped (as configured)

Configuration:
• WAF Enabled: ${{ env.WAF_ENABLED }}
• EXP Enabled: ${{ env.EXP }}

Run URL: ${RUN_URL}

Best regards,
Your Automation Team

", + "subject": "${{ env.accelerator_name }} Pipeline - Deployment Success" + } + EOF + ) + else + EMAIL_BODY=$(cat <Dear Team,

We would like to inform you that the ${{ env.accelerator_name }} deployment and testing process has completed successfully.

Deployment Details:
• Resource Group: ${RESOURCE_GROUP}
• Web App URL: ${WEBAPP_URL}
• E2E Tests: Passed ✅
• Test Suite: ${TEST_SUITE_NAME}
• Test Report: View Report

Configuration:
• WAF Enabled: ${{ env.WAF_ENABLED }}
• EXP Enabled: ${{ env.EXP }}

Run URL: ${RUN_URL}

Best regards,
Your Automation Team

", + "subject": "${{ env.accelerator_name }} Pipeline - Test Automation - Success" + } + EOF + ) + fi + + curl -X POST "${{ secrets.EMAILNOTIFICATION_LOGICAPP_URL_TA }}" \ + -H "Content-Type: application/json" \ + -d "$EMAIL_BODY" || echo "Failed to send success notification" + + - name: Send Test Failure Notification + if: inputs.deploy_result == 'success' && inputs.e2e_test_result != 'skipped' && inputs.TEST_SUCCESS != 'true' + shell: bash + run: | + RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + TEST_REPORT_URL="${{ inputs.TEST_REPORT_URL }}" + WEBAPP_URL="${{ inputs.WEB_APPURL || inputs.existing_webapp_url }}" + RESOURCE_GROUP="${{ inputs.RESOURCE_GROUP_NAME }}" + TEST_SUITE_NAME="${{ steps.test_suite.outputs.TEST_SUITE_NAME }}" + + EMAIL_BODY=$(cat <Dear Team,

We would like to inform you that ${{ env.accelerator_name }} accelerator test automation process has encountered issues and failed to complete successfully.

Deployment Details:
• Resource Group: ${RESOURCE_GROUP}
• Web App URL: ${WEBAPP_URL}
• Deployment Status: ✅ Success
• E2E Tests: ❌ Failed
• Test Suite: ${TEST_SUITE_NAME}

Test Details:
• Test Report: View Report

Run URL: ${RUN_URL}

Please investigate the matter at your earliest convenience.

Best regards,
Your Automation Team

", + "subject": "${{ env.accelerator_name }} Pipeline - Test Automation - Failed" + } + EOF + ) + + curl -X POST "${{ secrets.EMAILNOTIFICATION_LOGICAPP_URL_TA }}" \ + -H "Content-Type: application/json" \ + -d "$EMAIL_BODY" || echo "Failed to send test failure notification" + + - name: Send Existing URL Success Notification + if: inputs.deploy_result == 'skipped' && inputs.existing_webapp_url != '' && inputs.e2e_test_result == 'success' && (inputs.TEST_SUCCESS == 'true' || inputs.TEST_SUCCESS == '') + shell: bash + run: | + RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + EXISTING_URL="${{ inputs.existing_webapp_url }}" + TEST_REPORT_URL="${{ inputs.TEST_REPORT_URL }}" + TEST_SUITE_NAME="${{ steps.test_suite.outputs.TEST_SUITE_NAME }}" + + EMAIL_BODY=$(cat <Dear Team,

The ${{ env.accelerator_name }} pipeline executed against the existing WebApp URL and testing process has completed successfully.

Test Results:
• Status: ✅ Passed
• Test Suite: ${TEST_SUITE_NAME}
${TEST_REPORT_URL:+• Test Report: View Report}
• Target URL: ${EXISTING_URL}

Deployment: Skipped

Run URL: ${RUN_URL}

Best regards,
Your Automation Team

", + "subject": "${{ env.accelerator_name }} Pipeline - Test Automation Passed (Existing URL)" + } + EOF + ) + + curl -X POST "${{ secrets.EMAILNOTIFICATION_LOGICAPP_URL_TA }}" \ + -H "Content-Type: application/json" \ + -d "$EMAIL_BODY" || echo "Failed to send existing URL success notification" + + - name: Send Existing URL Test Failure Notification + if: inputs.deploy_result == 'skipped' && inputs.existing_webapp_url != '' && inputs.e2e_test_result == 'failure' + shell: bash + run: | + RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + EXISTING_URL="${{ inputs.existing_webapp_url }}" + TEST_REPORT_URL="${{ inputs.TEST_REPORT_URL }}" + TEST_SUITE_NAME="${{ steps.test_suite.outputs.TEST_SUITE_NAME }}" + + EMAIL_BODY=$(cat <Dear Team,

The ${{ env.accelerator_name }} pipeline executed against the existing WebApp URL and the test automation has encountered issues and failed to complete successfully.

Failure Details:
• Target URL: ${EXISTING_URL}
${TEST_REPORT_URL:+• Test Report: View Report}
• Test Suite: ${TEST_SUITE_NAME}
• Deployment: Skipped

Run URL: ${RUN_URL}

Best regards,
Your Automation Team

", + "subject": "${{ env.accelerator_name }} Pipeline - Test Automation Failed (Existing URL)" + } + EOF + ) + + curl -X POST "${{ secrets.EMAILNOTIFICATION_LOGICAPP_URL_TA }}" \ + -H "Content-Type: application/json" \ + -d "$EMAIL_BODY" || echo "Failed to send existing URL test failure notification" diff --git a/.github/workflows/test-automation-v2.yml b/.github/workflows/test-automation-v2.yml new file mode 100644 index 00000000..085693ba --- /dev/null +++ b/.github/workflows/test-automation-v2.yml @@ -0,0 +1,185 @@ +name: Test Automation DocGen-v2 + +on: + workflow_call: + inputs: + DOCGEN_URL: + required: true + type: string + description: "Web URL for DocGen" + TEST_SUITE: + required: false + type: string + default: "GoldenPath-Testing" + description: "Test suite to run: 'Smoke-Testing', 'GoldenPath-Testing' " + outputs: + TEST_SUCCESS: + description: "Whether tests passed" + value: ${{ jobs.test.outputs.TEST_SUCCESS }} + TEST_REPORT_URL: + description: "URL to test report artifact" + value: ${{ jobs.test.outputs.TEST_REPORT_URL }} + +env: + url: ${{ inputs.DOCGEN_URL }} + accelerator_name: "DocGen" + test_suite: ${{ inputs.TEST_SUITE }} + +jobs: + test: + runs-on: ubuntu-latest + outputs: + TEST_SUCCESS: ${{ steps.test1.outcome == 'success' || steps.test2.outcome == 'success' || steps.test3.outcome == 'success' }} + TEST_REPORT_URL: ${{ steps.upload_report.outputs.artifact-url }} + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.13' + + - name: Login to Azure + run: | + az login --service-principal -u ${{ secrets.AZURE_CLIENT_ID }} -p ${{ secrets.AZURE_CLIENT_SECRET }} --tenant ${{ secrets.AZURE_TENANT_ID }} + az account set --subscription ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r tests/e2e-test/requirements.txt + + - name: Ensure browsers are installed + run: python -m playwright install --with-deps chromium + + - name: Validate URL + run: | + if [ -z "${{ env.url }}" ]; then + echo "ERROR: No URL provided for testing" + exit 1 + fi + echo "Testing URL: ${{ env.url }}" + echo "Test Suite: ${{ env.test_suite }}" + + + - name: Wait for Application to be Ready + run: | + echo "Waiting for application to be ready at ${{ env.url }} " + max_attempts=10 + attempt=1 + + while [ $attempt -le $max_attempts ]; do + echo "Attempt $attempt: Checking if application is ready..." + if curl -f -s "${{ env.url }}" > /dev/null; then + echo "Application is ready!" + break + + fi + + if [ $attempt -eq $max_attempts ]; then + echo "Application is not ready after $max_attempts attempts" + exit 1 + fi + + echo "Application not ready, waiting 30 seconds..." + sleep 30 + attempt=$((attempt + 1)) + done + + - name: Run tests(1) + id: test1 + run: | + if [ "${{ env.test_suite }}" == "GoldenPath-Testing" ]; then + xvfb-run pytest --headed --html=report/report.html --self-contained-html + fi + working-directory: tests/e2e-test + continue-on-error: true + + - name: Sleep for 30 seconds + if: ${{ steps.test1.outcome == 'failure' }} + run: sleep 30s + shell: bash + + - name: Run tests(2) + id: test2 + if: ${{ steps.test1.outcome == 'failure' }} + run: | + if [ "${{ env.test_suite }}" == "GoldenPath-Testing" ]; then + xvfb-run pytest --headed --html=report/report.html --self-contained-html + fi + working-directory: tests/e2e-test + continue-on-error: true + + - name: Sleep for 60 seconds + if: ${{ steps.test2.outcome == 'failure' }} + run: sleep 60s + shell: bash + + - name: Run tests(3) + id: test3 + if: ${{ steps.test2.outcome == 'failure' }} + run: | + if [ "${{ env.test_suite }}" == "GoldenPath-Testing" ]; then + xvfb-run pytest --headed --html=report/report.html --self-contained-html + fi + working-directory: tests/e2e-test + + - name: Upload test report + id: upload_report + uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: test-report + path: tests/e2e-test/report/* + + - name: Generate E2E Test Summary + if: always() + run: | + # Determine test suite type for title + if [ "${{ env.test_suite }}" == "GoldenPath-Testing" ]; then + echo "## 🧪 E2E Test Job Summary : Golden Path Testing" >> $GITHUB_STEP_SUMMARY + else + echo "## 🧪 E2E Test Job Summary : Smoke Testing" >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY + + # Determine overall test result + OVERALL_SUCCESS="${{ steps.test1.outcome == 'success' || steps.test2.outcome == 'success' || steps.test3.outcome == 'success' }}" + if [[ "$OVERALL_SUCCESS" == "true" ]]; then + echo "| **Job Status** | ✅ Success |" >> $GITHUB_STEP_SUMMARY + else + echo "| **Job Status** | ❌ Failed |" >> $GITHUB_STEP_SUMMARY + fi + + echo "| **Target URL** | [${{ env.url }}](${{ env.url }}) |" >> $GITHUB_STEP_SUMMARY + echo "| **Test Suite** | \`${{ env.test_suite }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Test Report** | [Download Artifact](${{ steps.upload_report.outputs.artifact-url }}) |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + echo "### 📋 Test Execution Details" >> $GITHUB_STEP_SUMMARY + echo "| Attempt | Status | Notes |" >> $GITHUB_STEP_SUMMARY + echo "|---------|--------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| **Test Run 1** | ${{ steps.test1.outcome == 'success' && '✅ Passed' || '❌ Failed' }} | Initial test execution |" >> $GITHUB_STEP_SUMMARY + + if [[ "${{ steps.test1.outcome }}" == "failure" ]]; then + echo "| **Test Run 2** | ${{ steps.test2.outcome == 'success' && '✅ Passed' || steps.test2.outcome == 'failure' && '❌ Failed' || '⏸️ Skipped' }} | Retry after 30s delay |" >> $GITHUB_STEP_SUMMARY + fi + + if [[ "${{ steps.test2.outcome }}" == "failure" ]]; then + echo "| **Test Run 3** | ${{ steps.test3.outcome == 'success' && '✅ Passed' || steps.test3.outcome == 'failure' && '❌ Failed' || '⏸️ Skipped' }} | Final retry after 60s delay |" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + + if [[ "$OVERALL_SUCCESS" == "true" ]]; then + echo "### ✅ Test Results" >> $GITHUB_STEP_SUMMARY + echo "- End-to-end tests completed successfully" >> $GITHUB_STEP_SUMMARY + echo "- Application is functioning as expected" >> $GITHUB_STEP_SUMMARY + else + echo "### ❌ Test Results" >> $GITHUB_STEP_SUMMARY + echo "- All test attempts failed" >> $GITHUB_STEP_SUMMARY + echo "- Check the e2e-test/test job for detailed error information" >> $GITHUB_STEP_SUMMARY + fi \ No newline at end of file From c31425c8d35f208f1b146b5aa516cb8fb49790ce Mon Sep 17 00:00:00 2001 From: Rafi-Microsoft Date: Mon, 15 Dec 2025 17:41:01 +0530 Subject: [PATCH 04/49] removed linux reference --- .github/workflows/job-deploy.yml | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/.github/workflows/job-deploy.yml b/.github/workflows/job-deploy.yml index fc77e6eb..37087128 100644 --- a/.github/workflows/job-deploy.yml +++ b/.github/workflows/job-deploy.yml @@ -72,7 +72,7 @@ on: value: ${{ jobs.azure-setup.outputs.RESOURCE_GROUP_NAME }} WEB_APPURL: description: "Container Web App URL" - value: ${{ jobs.deploy-linux.outputs.WEB_APPURL || jobs.deploy-windows.outputs.WEB_APPURL }} + value: ${{ jobs.deploy-windows.outputs.WEB_APPURL }} ENV_NAME: description: "Environment Name" value: ${{ jobs.azure-setup.outputs.ENV_NAME }} @@ -325,24 +325,6 @@ jobs: echo "ℹ️ **Note:** Manual Trigger - Using user-specified configuration" >> $GITHUB_STEP_SUMMARY fi - deploy-linux: - name: Deploy on Linux - needs: azure-setup - if: inputs.runner_os == 'ubuntu-latest' && always() && needs.azure-setup.result == 'success' - uses: ./.github/workflows/job-deploy-linux.yml - with: - ENV_NAME: ${{ needs.azure-setup.outputs.ENV_NAME }} - AZURE_ENV_OPENAI_LOCATION: ${{ needs.azure-setup.outputs.AZURE_ENV_OPENAI_LOCATION }} - AZURE_LOCATION: ${{ needs.azure-setup.outputs.AZURE_LOCATION }} - RESOURCE_GROUP_NAME: ${{ needs.azure-setup.outputs.RESOURCE_GROUP_NAME }} - IMAGE_TAG: ${{ needs.azure-setup.outputs.IMAGE_TAG }} - BUILD_DOCKER_IMAGE: ${{ inputs.build_docker_image || 'false' }} - EXP: ${{ inputs.EXP || 'false' }} - WAF_ENABLED: ${{ inputs.waf_enabled == true && 'true' || 'false' }} - AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }} - AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }} - secrets: inherit - deploy-windows: name: Deploy on Windows needs: azure-setup From ebfd02f3d24dbd05d2a7112bfbcf1487a9a149f9 Mon Sep 17 00:00:00 2001 From: Rafi-Microsoft Date: Mon, 15 Dec 2025 17:45:54 +0530 Subject: [PATCH 05/49] fix v1 --- .github/workflows/job-deploy.yml | 43 +++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/.github/workflows/job-deploy.yml b/.github/workflows/job-deploy.yml index 37087128..34f61a9c 100644 --- a/.github/workflows/job-deploy.yml +++ b/.github/workflows/job-deploy.yml @@ -143,22 +143,37 @@ jobs: - name: Run Quota Check id: quota-check + shell: pwsh run: | - export AZURE_CLIENT_ID=${{ secrets.AZURE_CLIENT_ID }} - export AZURE_TENANT_ID=${{ secrets.AZURE_TENANT_ID }} - export AZURE_CLIENT_SECRET=${{ secrets.AZURE_CLIENT_SECRET }} - export AZURE_SUBSCRIPTION_ID="${{ secrets.AZURE_SUBSCRIPTION_ID }}" - export GPT_MIN_CAPACITY=${{ env.GPT_MIN_CAPACITY }} - export TEXT_EMBEDDING_MIN_CAPACITY=${{ env.TEXT_EMBEDDING_MIN_CAPACITY }} - export AZURE_REGIONS="${{ vars.AZURE_REGIONS }}" + $ErrorActionPreference = "Stop" # Ensure that any error stops the pipeline - chmod +x scripts/checkquota.sh - if ! scripts/checkquota.sh; then - if grep -q "No region with sufficient quota found" scripts/checkquota.sh; then - echo "QUOTA_FAILED=true" >> $GITHUB_ENV - fi - exit 1 - fi + # Path to the PowerShell script for quota check + $quotaCheckScript = "Deployment/checkquota.ps1" + + # Check if the script exists and is executable (not needed for PowerShell like chmod) + if (-not (Test-Path $quotaCheckScript)) { + Write-Host "❌ Error: Quota check script not found." + exit 1 + } + + # Run the script + .\Deployment\checkquota.ps1 + + # If the script fails, check for the failure message + $quotaFailedMessage = "No region with sufficient quota found" + $output = Get-Content "Deployment/checkquota.ps1" + + if ($output -contains $quotaFailedMessage) { + echo "QUOTA_FAILED=true" >> $GITHUB_ENV + } + env: + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} + GPT_MIN_CAPACITY: ${{ env.GPT_CAPACITY }} + TEXT_EMBEDDING_MIN_CAPACITY: ${{ env.TEXT_EMBEDDING_CAPACITY }} + AZURE_REGIONS: "${{ vars.AZURE_REGIONS }}" - name: Set Quota Failure Output id: quota_failure_output From 793bbb430e9dfabbf2d9983e212e3bc7cab72c8c Mon Sep 17 00:00:00 2001 From: Rafi-Microsoft Date: Mon, 15 Dec 2025 17:54:23 +0530 Subject: [PATCH 06/49] fix v2 --- .github/workflows/job-deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/job-deploy.yml b/.github/workflows/job-deploy.yml index 34f61a9c..af2e534e 100644 --- a/.github/workflows/job-deploy.yml +++ b/.github/workflows/job-deploy.yml @@ -171,8 +171,8 @@ jobs: AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} - GPT_MIN_CAPACITY: ${{ env.GPT_CAPACITY }} - TEXT_EMBEDDING_MIN_CAPACITY: ${{ env.TEXT_EMBEDDING_CAPACITY }} + GPT_MIN_CAPACITY: ${{ env.GPT_MIN_CAPACITY }} + TEXT_EMBEDDING_MIN_CAPACITY: ${{ env.TEXT_EMBEDDING_MIN_CAPACITY }} AZURE_REGIONS: "${{ vars.AZURE_REGIONS }}" - name: Set Quota Failure Output From e947d3ebdd8bc0725b31a5a5f610e3723663faa2 Mon Sep 17 00:00:00 2001 From: Rafi-Microsoft Date: Tue, 16 Dec 2025 11:38:32 +0530 Subject: [PATCH 07/49] post deployment fix --- .github/workflows/job-deploy-windows.yml | 34 ++++++++++++------------ 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/.github/workflows/job-deploy-windows.yml b/.github/workflows/job-deploy-windows.yml index 09af1ad5..dd2ab9c2 100644 --- a/.github/workflows/job-deploy-windows.yml +++ b/.github/workflows/job-deploy-windows.yml @@ -209,24 +209,24 @@ jobs: - name: Run Post-Deployment Script id: post_deploy - env: - AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - shell: bash + shell: pwsh run: | - set -e - az account set --subscription "${{ secrets.AZURE_SUBSCRIPTION_ID }}" - - echo "Running post-deployment script..." - - bash ./infra/scripts/process_sample_data.sh \ - "${{ env.STORAGE_ACCOUNT_NAME }}" \ - "${{ env.STORAGE_CONTAINER_NAME }}" \ - "${{ env.KEY_VAULT_NAME }}" \ - "${{ env.AZURE_COSMOSDB_ACCOUNT }}" \ - "${{ env.RESOURCE_GROUP_NAME }}" \ - "${{ env.AI_SEARCH_SERVICE_NAME }}" \ - "${{ secrets.AZURE_CLIENT_ID }}" \ - "${{ env.AI_FOUNDRY_RESOURCE_ID }}" + Write-Host "Running post deployment script to upload files..." + cd Deployment + try { + .\uploadfiles.ps1 -EndpointUrl ${{ env.WEB_APPURL }} + Write-Host "ExitCode: $LASTEXITCODE" + if ($LASTEXITCODE -eq $null -or $LASTEXITCODE -eq 0) { + Write-Host "✅ Post deployment script completed successfully." + } else { + Write-Host "❌ Post deployment script failed with exit code: $LASTEXITCODE" + exit 1 + } + } + catch { + Write-Host "❌ Post deployment script failed with error: $($_.Exception.Message)" + exit 1 + } - name: Generate Deploy Job Summary if: always() From 0daefc5bf36ac7c3a719b2fbc212b3eaee867887 Mon Sep 17 00:00:00 2001 From: Rafi-Microsoft Date: Tue, 16 Dec 2025 11:55:45 +0530 Subject: [PATCH 08/49] post dep fix v2 --- .github/workflows/job-deploy-windows.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/job-deploy-windows.yml b/.github/workflows/job-deploy-windows.yml index dd2ab9c2..fbc7fc23 100644 --- a/.github/workflows/job-deploy-windows.yml +++ b/.github/workflows/job-deploy-windows.yml @@ -214,7 +214,7 @@ jobs: Write-Host "Running post deployment script to upload files..." cd Deployment try { - .\uploadfiles.ps1 -EndpointUrl ${{ env.WEB_APPURL }} + .\uploadfiles.ps1 -EndpointUrl $env:WEB_APPURL Write-Host "ExitCode: $LASTEXITCODE" if ($LASTEXITCODE -eq $null -or $LASTEXITCODE -eq 0) { Write-Host "✅ Post deployment script completed successfully." From 56ffec847f5c2b79d9ce22b3a98c5bf3b17c1961 Mon Sep 17 00:00:00 2001 From: Rafi-Microsoft Date: Tue, 16 Dec 2025 14:16:11 +0530 Subject: [PATCH 09/49] post dep fix v3 --- .github/workflows/job-deploy-windows.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/workflows/job-deploy-windows.yml b/.github/workflows/job-deploy-windows.yml index fbc7fc23..23c710f0 100644 --- a/.github/workflows/job-deploy-windows.yml +++ b/.github/workflows/job-deploy-windows.yml @@ -204,6 +204,14 @@ jobs: "RESOURCE_GROUP_NAME=$RESOURCE_GROUP_NAME" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append $WEB_APP_URL = $DEPLOY_OUTPUT.WEB_APP_URL + Write-Host "WEB_APP_URL extracted: $WEB_APP_URL" + + if ([string]::IsNullOrWhiteSpace($WEB_APP_URL)) { + Write-Host "❌ Error: WEB_APP_URL is empty or not found in deployment output." + Write-Host "Available output keys: $($DEPLOY_OUTPUT.PSObject.Properties.Name -join ', ')" + exit 1 + } + "WEB_APPURL=$WEB_APP_URL" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append "WEB_APPURL=$WEB_APP_URL" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append @@ -212,6 +220,13 @@ jobs: shell: pwsh run: | Write-Host "Running post deployment script to upload files..." + Write-Host "WEB_APPURL value: $env:WEB_APPURL" + + if ([string]::IsNullOrWhiteSpace($env:WEB_APPURL)) { + Write-Host "❌ Error: WEB_APPURL environment variable is empty." + exit 1 + } + cd Deployment try { .\uploadfiles.ps1 -EndpointUrl $env:WEB_APPURL From d6a39cb152f57a9a9414828def735a89a74a60a7 Mon Sep 17 00:00:00 2001 From: Rafi-Microsoft Date: Tue, 16 Dec 2025 15:16:53 +0530 Subject: [PATCH 10/49] pipeline fix v1 --- .github/workflows/job-deploy-windows.yml | 173 ++++++++++++++++++----- 1 file changed, 138 insertions(+), 35 deletions(-) diff --git a/.github/workflows/job-deploy-windows.yml b/.github/workflows/job-deploy-windows.yml index 23c710f0..0cdbe5f8 100644 --- a/.github/workflows/job-deploy-windows.yml +++ b/.github/workflows/job-deploy-windows.yml @@ -167,9 +167,18 @@ jobs: } # Deploy using azd up - azd up --no-prompt - - Write-Host "✅ Deployment succeeded." + Write-Host "Starting deployment with azd up..." + try { + azd up --no-prompt + if ($LASTEXITCODE -ne 0) { + Write-Host "❌ Deployment failed with exit code: $LASTEXITCODE" + exit 1 + } + Write-Host "✅ Deployment succeeded." + } catch { + Write-Host "❌ Deployment failed with error: $($_.Exception.Message)" + exit 1 + } # Get deployment outputs using azd Write-Host "Extracting deployment outputs..." @@ -177,53 +186,147 @@ jobs: Write-Host "Deployment output: $($DEPLOY_OUTPUT | ConvertTo-Json -Depth 10)" if (-not $DEPLOY_OUTPUT) { - Write-Host "Error: Deployment output is empty. Please check the deployment logs." + Write-Host "❌ Error: Deployment output is empty. Please check the deployment logs." exit 1 } - - $AI_FOUNDRY_RESOURCE_ID = $DEPLOY_OUTPUT.AI_FOUNDRY_RESOURCE_ID - "AI_FOUNDRY_RESOURCE_ID=$AI_FOUNDRY_RESOURCE_ID" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - - $AI_SEARCH_SERVICE_NAME = $DEPLOY_OUTPUT.AI_SEARCH_SERVICE_NAME - "AI_SEARCH_SERVICE_NAME=$AI_SEARCH_SERVICE_NAME" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - - $AZURE_COSMOSDB_ACCOUNT = $DEPLOY_OUTPUT.AZURE_COSMOSDB_ACCOUNT - "AZURE_COSMOSDB_ACCOUNT=$AZURE_COSMOSDB_ACCOUNT" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - - $STORAGE_ACCOUNT_NAME = $DEPLOY_OUTPUT.STORAGE_ACCOUNT_NAME - "STORAGE_ACCOUNT_NAME=$STORAGE_ACCOUNT_NAME" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - - $STORAGE_CONTAINER_NAME = $DEPLOY_OUTPUT.STORAGE_CONTAINER_NAME - "STORAGE_CONTAINER_NAME=$STORAGE_CONTAINER_NAME" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - - $KEY_VAULT_NAME = $DEPLOY_OUTPUT.KEY_VAULT_NAME - "KEY_VAULT_NAME=$KEY_VAULT_NAME" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - - $RESOURCE_GROUP_NAME = $DEPLOY_OUTPUT.RESOURCE_GROUP_NAME - "RESOURCE_GROUP_NAME=$RESOURCE_GROUP_NAME" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - - $WEB_APP_URL = $DEPLOY_OUTPUT.WEB_APP_URL - Write-Host "WEB_APP_URL extracted: $WEB_APP_URL" + # Save all deployment outputs to GITHUB_ENV + Write-Host "Saving deployment outputs to environment variables..." + "RESOURCE_GROUP_NAME=$($DEPLOY_OUTPUT.RESOURCE_GROUP_NAME)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "AZURE_RESOURCE_GROUP_ID=$($DEPLOY_OUTPUT.AZURE_RESOURCE_GROUP_ID)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "STORAGE_ACCOUNT_NAME=$($DEPLOY_OUTPUT.STORAGE_ACCOUNT_NAME)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "AZURE_SEARCH_SERVICE_NAME=$($DEPLOY_OUTPUT.AZURE_SEARCH_SERVICE_NAME)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "AZURE_AKS_NAME=$($DEPLOY_OUTPUT.AZURE_AKS_NAME)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "AZURE_AKS_MI_ID=$($DEPLOY_OUTPUT.AZURE_AKS_MI_ID)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "AZURE_CONTAINER_REGISTRY_NAME=$($DEPLOY_OUTPUT.AZURE_CONTAINER_REGISTRY_NAME)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "AZURE_COGNITIVE_SERVICE_NAME=$($DEPLOY_OUTPUT.AZURE_COGNITIVE_SERVICE_NAME)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "AZURE_COGNITIVE_SERVICE_ENDPOINT=$($DEPLOY_OUTPUT.AZURE_COGNITIVE_SERVICE_ENDPOINT)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "AZURE_OPENAI_SERVICE_NAME=$($DEPLOY_OUTPUT.AZURE_OPENAI_SERVICE_NAME)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "AZURE_OPENAI_SERVICE_ENDPOINT=$($DEPLOY_OUTPUT.AZURE_OPENAI_SERVICE_ENDPOINT)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "AZURE_COSMOSDB_NAME=$($DEPLOY_OUTPUT.AZURE_COSMOSDB_NAME)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "AZ_GPT4O_MODEL_NAME=$($DEPLOY_OUTPUT.AZ_GPT4O_MODEL_NAME)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "AZ_GPT4O_MODEL_ID=$($DEPLOY_OUTPUT.AZ_GPT4O_MODEL_ID)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "AZ_GPT_EMBEDDING_MODEL_NAME=$($DEPLOY_OUTPUT.AZ_GPT_EMBEDDING_MODEL_NAME)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "AZ_GPT_EMBEDDING_MODEL_ID=$($DEPLOY_OUTPUT.AZ_GPT_EMBEDDING_MODEL_ID)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "AZURE_APP_CONFIG_ENDPOINT=$($DEPLOY_OUTPUT.AZURE_APP_CONFIG_ENDPOINT)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "AZURE_APP_CONFIG_NAME=$($DEPLOY_OUTPUT.AZURE_APP_CONFIG_NAME)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + + # Get AKS managed resource group (node resource group) + Write-Host "Retrieving AKS managed resource group..." + $AKS_NAME = $DEPLOY_OUTPUT.AZURE_AKS_NAME + $RESOURCE_GROUP = $DEPLOY_OUTPUT.RESOURCE_GROUP_NAME + $AKS_NODE_RG = az aks show --name $AKS_NAME --resource-group $RESOURCE_GROUP --query "nodeResourceGroup" -o tsv + Write-Host "AKS node resource group: $AKS_NODE_RG" + "krg_name=$AKS_NODE_RG" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + + # Get Web App URL from AKS public IP + Write-Host "Retrieving Web App URL from AKS..." + $PUBLIC_IP_NAME = az network public-ip list --resource-group $AKS_NODE_RG --query "[?contains(name, 'kubernetes-')].name" -o tsv | Select-Object -First 1 + + if ([string]::IsNullOrWhiteSpace($PUBLIC_IP_NAME)) { + Write-Host "❌ Error: Could not find public IP in resource group $AKS_NODE_RG" + exit 1 + } - if ([string]::IsNullOrWhiteSpace($WEB_APP_URL)) { - Write-Host "❌ Error: WEB_APP_URL is empty or not found in deployment output." - Write-Host "Available output keys: $($DEPLOY_OUTPUT.PSObject.Properties.Name -join ', ')" + Write-Host "Found public IP: $PUBLIC_IP_NAME" + $FQDN = az network public-ip show --resource-group $AKS_NODE_RG --name $PUBLIC_IP_NAME --query "dnsSettings.fqdn" -o tsv + + if ([string]::IsNullOrWhiteSpace($FQDN)) { + Write-Host "❌ Error: Could not retrieve FQDN for public IP $PUBLIC_IP_NAME" exit 1 } + $WEB_APP_URL = "https://$FQDN" + Write-Host "Web App URL: $WEB_APP_URL" + "WEB_APPURL=$WEB_APP_URL" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append "WEB_APPURL=$WEB_APP_URL" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append - - name: Run Post-Deployment Script - id: post_deploy + - name: Run Deployment Script with Input + shell: pwsh + run: | + cd Deployment + $input = @" + ${{ secrets.EMAIL }} + yes + "@ + $input | pwsh ./resourcedeployment.ps1 + Write-Host "Resource Group Name is ${{ env.RESOURCE_GROUP_NAME }}" + Write-Host "Kubernetes resource group is ${{ env.AZURE_AKS_NAME }}" + env: + # From GitHub secrets (for login) + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} + + # From deployment outputs step (these come from $GITHUB_ENV) + RESOURCE_GROUP_NAME: ${{ env.RESOURCE_GROUP_NAME }} + AZURE_RESOURCE_GROUP_ID: ${{ env.AZURE_RESOURCE_GROUP_ID }} + STORAGE_ACCOUNT_NAME: ${{ env.STORAGE_ACCOUNT_NAME }} + AZURE_SEARCH_SERVICE_NAME: ${{ env.AZURE_SEARCH_SERVICE_NAME }} + AZURE_AKS_NAME: ${{ env.AZURE_AKS_NAME }} + AZURE_AKS_MI_ID: ${{ env.AZURE_AKS_MI_ID }} + AZURE_CONTAINER_REGISTRY_NAME: ${{ env.AZURE_CONTAINER_REGISTRY_NAME }} + AZURE_COGNITIVE_SERVICE_NAME: ${{ env.AZURE_COGNITIVE_SERVICE_NAME }} + AZURE_COGNITIVE_SERVICE_ENDPOINT: ${{ env.AZURE_COGNITIVE_SERVICE_ENDPOINT }} + AZURE_OPENAI_SERVICE_NAME: ${{ env.AZURE_OPENAI_SERVICE_NAME }} + AZURE_OPENAI_SERVICE_ENDPOINT: ${{ env.AZURE_OPENAI_SERVICE_ENDPOINT }} + AZURE_COSMOSDB_NAME: ${{ env.AZURE_COSMOSDB_NAME }} + AZ_GPT4O_MODEL_NAME: ${{ env.AZ_GPT4O_MODEL_NAME }} + AZ_GPT4O_MODEL_ID: ${{ env.AZ_GPT4O_MODEL_ID }} + AZ_GPT_EMBEDDING_MODEL_NAME: ${{ env.AZ_GPT_EMBEDDING_MODEL_NAME }} + AZ_GPT_EMBEDDING_MODEL_ID: ${{ env.AZ_GPT_EMBEDDING_MODEL_ID }} + AZURE_APP_CONFIG_ENDPOINT: ${{ env.AZURE_APP_CONFIG_ENDPOINT }} + AZURE_APP_CONFIG_NAME: ${{ env.AZURE_APP_CONFIG_NAME }} + + - name: Validate Deployment + shell: bash + run: | + webapp_url="${{ env.WEB_APPURL }}" + echo "Validating web app at: $webapp_url" + + # Enhanced health check with retry logic + max_attempts=7 + attempt=1 + success=false + + while [ $attempt -le $max_attempts ] && [ "$success" = false ]; do + echo "Attempt $attempt/$max_attempts: Checking web app health..." + + # Check if web app responds + http_code=$(curl -s -o /dev/null -w "%{http_code}" "$webapp_url" || echo "000") + + if [ "$http_code" -eq 200 ]; then + echo "✅ Web app is healthy (HTTP $http_code)" + success=true + elif [ "$http_code" -eq 404 ]; then + echo "❌ Web app not found (HTTP 404)" + break + elif [ "$http_code" -eq 503 ] || [ "$http_code" -eq 502 ]; then + echo "⚠️ Web app temporarily unavailable (HTTP $http_code), retrying..." + sleep 20 + else + echo "⚠️ Web app returned HTTP $http_code, retrying..." + sleep 20 + fi + + attempt=$((attempt + 1)) + done + + if [ "$success" = false ]; then + echo "❌ Web app validation failed after $max_attempts attempts" + exit 1 + fi + + - name: Run Post Deployment Script shell: pwsh run: | Write-Host "Running post deployment script to upload files..." - Write-Host "WEB_APPURL value: $env:WEB_APPURL" + Write-Host "WEB_APPURL: $env:WEB_APPURL" if ([string]::IsNullOrWhiteSpace($env:WEB_APPURL)) { - Write-Host "❌ Error: WEB_APPURL environment variable is empty." + Write-Host "❌ Error: WEB_APPURL is empty" exit 1 } From 39fd71ae10b7965599acfe40039554d578790968 Mon Sep 17 00:00:00 2001 From: Rafi-Microsoft Date: Tue, 16 Dec 2025 15:56:36 +0530 Subject: [PATCH 11/49] pipeline fix v2 --- .github/workflows/job-deploy-windows.yml | 101 +++++++++++++++++------ 1 file changed, 77 insertions(+), 24 deletions(-) diff --git a/.github/workflows/job-deploy-windows.yml b/.github/workflows/job-deploy-windows.yml index 0cdbe5f8..67a97a7c 100644 --- a/.github/workflows/job-deploy-windows.yml +++ b/.github/workflows/job-deploy-windows.yml @@ -211,7 +211,7 @@ jobs: "AZURE_APP_CONFIG_ENDPOINT=$($DEPLOY_OUTPUT.AZURE_APP_CONFIG_ENDPOINT)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append "AZURE_APP_CONFIG_NAME=$($DEPLOY_OUTPUT.AZURE_APP_CONFIG_NAME)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - # Get AKS managed resource group (node resource group) + # Get AKS managed resource group (node resource group) and save for later use Write-Host "Retrieving AKS managed resource group..." $AKS_NAME = $DEPLOY_OUTPUT.AZURE_AKS_NAME $RESOURCE_GROUP = $DEPLOY_OUTPUT.RESOURCE_GROUP_NAME @@ -219,29 +219,6 @@ jobs: Write-Host "AKS node resource group: $AKS_NODE_RG" "krg_name=$AKS_NODE_RG" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - # Get Web App URL from AKS public IP - Write-Host "Retrieving Web App URL from AKS..." - $PUBLIC_IP_NAME = az network public-ip list --resource-group $AKS_NODE_RG --query "[?contains(name, 'kubernetes-')].name" -o tsv | Select-Object -First 1 - - if ([string]::IsNullOrWhiteSpace($PUBLIC_IP_NAME)) { - Write-Host "❌ Error: Could not find public IP in resource group $AKS_NODE_RG" - exit 1 - } - - Write-Host "Found public IP: $PUBLIC_IP_NAME" - $FQDN = az network public-ip show --resource-group $AKS_NODE_RG --name $PUBLIC_IP_NAME --query "dnsSettings.fqdn" -o tsv - - if ([string]::IsNullOrWhiteSpace($FQDN)) { - Write-Host "❌ Error: Could not retrieve FQDN for public IP $PUBLIC_IP_NAME" - exit 1 - } - - $WEB_APP_URL = "https://$FQDN" - Write-Host "Web App URL: $WEB_APP_URL" - - "WEB_APPURL=$WEB_APP_URL" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - "WEB_APPURL=$WEB_APP_URL" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append - - name: Run Deployment Script with Input shell: pwsh run: | @@ -280,6 +257,82 @@ jobs: AZURE_APP_CONFIG_ENDPOINT: ${{ env.AZURE_APP_CONFIG_ENDPOINT }} AZURE_APP_CONFIG_NAME: ${{ env.AZURE_APP_CONFIG_NAME }} + - name: Retrieve Web App URL + id: get_webapp_url + shell: pwsh + run: | + Write-Host "Retrieving Web App URL from AKS..." + Write-Host "Kubernetes resource group: $env:krg_name" + + # Get Web App URL from AKS public IP with retry logic + $maxAttempts = 12 + $attempt = 1 + $PUBLIC_IP_NAME = $null + + while ($attempt -le $maxAttempts) { + Write-Host "Attempt $attempt/$maxAttempts : Checking for public IP..." + + # List all public IPs in the resource group for debugging + $allIPs = az network public-ip list --resource-group $env:krg_name --query "[].name" -o tsv + if ($allIPs) { + Write-Host "Available public IPs: $($allIPs -join ', ')" + } else { + Write-Host "No public IPs found yet in resource group $env:krg_name" + } + + # Try to find kubernetes public IP + $PUBLIC_IP_NAME = az network public-ip list --resource-group $env:krg_name --query "[?contains(name, 'kubernetes-')].name" -o tsv | Select-Object -First 1 + + if (-not [string]::IsNullOrWhiteSpace($PUBLIC_IP_NAME)) { + Write-Host "✅ Found public IP: $PUBLIC_IP_NAME" + break + } + + if ($attempt -lt $maxAttempts) { + Write-Host "Public IP not found yet, waiting 30 seconds..." + Start-Sleep -Seconds 30 + } + $attempt++ + } + + if ([string]::IsNullOrWhiteSpace($PUBLIC_IP_NAME)) { + Write-Host "❌ Error: Could not find public IP after $maxAttempts attempts in resource group $env:krg_name" + exit 1 + } + + # Get FQDN with retry + Write-Host "Retrieving FQDN for public IP: $PUBLIC_IP_NAME" + $FQDN = $null + $fqdnAttempts = 5 + $fqdnAttempt = 1 + + while ($fqdnAttempt -le $fqdnAttempts) { + Write-Host "Attempt $fqdnAttempt/$fqdnAttempts : Getting FQDN..." + $FQDN = az network public-ip show --resource-group $env:krg_name --name $PUBLIC_IP_NAME --query "dnsSettings.fqdn" -o tsv + + if (-not [string]::IsNullOrWhiteSpace($FQDN)) { + Write-Host "✅ Found FQDN: $FQDN" + break + } + + if ($fqdnAttempt -lt $fqdnAttempts) { + Write-Host "FQDN not available yet, waiting 15 seconds..." + Start-Sleep -Seconds 15 + } + $fqdnAttempt++ + } + + if ([string]::IsNullOrWhiteSpace($FQDN)) { + Write-Host "❌ Error: Could not retrieve FQDN for public IP $PUBLIC_IP_NAME after $fqdnAttempts attempts" + exit 1 + } + + $WEB_APP_URL = "https://$FQDN" + Write-Host "✅ Web App URL: $WEB_APP_URL" + + "WEB_APPURL=$WEB_APP_URL" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "WEB_APPURL=$WEB_APP_URL" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + - name: Validate Deployment shell: bash run: | From 14c9d00f1eeba53fecf4b5b858bb210a8f7d3603 Mon Sep 17 00:00:00 2001 From: Rafi-Microsoft Date: Tue, 16 Dec 2025 16:14:34 +0530 Subject: [PATCH 12/49] fix v3 --- .github/workflows/job-deploy-windows.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/job-deploy-windows.yml b/.github/workflows/job-deploy-windows.yml index 67a97a7c..ba84e8c6 100644 --- a/.github/workflows/job-deploy-windows.yml +++ b/.github/workflows/job-deploy-windows.yml @@ -133,6 +133,10 @@ jobs: azd env set AZURE_RESOURCE_GROUP="${{ inputs.RESOURCE_GROUP_NAME }}" azd env set AZURE_ENV_IMAGETAG="${{ inputs.IMAGE_TAG }}" + # Set infrastructure parameters that azd expects + azd env set aiDeploymentsLocation="${{ inputs.AZURE_ENV_OPENAI_LOCATION }}" + azd env set location="${{ inputs.AZURE_LOCATION }}" + # Set ACR name only when building Docker image if ("${{ inputs.BUILD_DOCKER_IMAGE }}" -eq "true") { $ACR_NAME = "${{ secrets.ACR_TEST_USERNAME }}" From bd1917983c6adb0f2db26f9b1882d214ba5842f1 Mon Sep 17 00:00:00 2001 From: Rafi-Microsoft Date: Tue, 16 Dec 2025 16:43:12 +0530 Subject: [PATCH 13/49] fix v4 --- .github/workflows/job-deploy-windows.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/job-deploy-windows.yml b/.github/workflows/job-deploy-windows.yml index ba84e8c6..57bf49aa 100644 --- a/.github/workflows/job-deploy-windows.yml +++ b/.github/workflows/job-deploy-windows.yml @@ -133,8 +133,8 @@ jobs: azd env set AZURE_RESOURCE_GROUP="${{ inputs.RESOURCE_GROUP_NAME }}" azd env set AZURE_ENV_IMAGETAG="${{ inputs.IMAGE_TAG }}" - # Set infrastructure parameters that azd expects - azd env set aiDeploymentsLocation="${{ inputs.AZURE_ENV_OPENAI_LOCATION }}" + # Set infrastructure parameters that azd expects (both use AZURE_LOCATION like CI.yml) + azd env set aiDeploymentsLocation="${{ inputs.AZURE_LOCATION }}" azd env set location="${{ inputs.AZURE_LOCATION }}" # Set ACR name only when building Docker image From 9cfa334ea31dcfbd36cd42369f957e951c4d670b Mon Sep 17 00:00:00 2001 From: Rafi-Microsoft Date: Tue, 16 Dec 2025 17:23:30 +0530 Subject: [PATCH 14/49] updated ai location --- .github/workflows/job-deploy-windows.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/job-deploy-windows.yml b/.github/workflows/job-deploy-windows.yml index 57bf49aa..3a334525 100644 --- a/.github/workflows/job-deploy-windows.yml +++ b/.github/workflows/job-deploy-windows.yml @@ -128,7 +128,7 @@ jobs: # Set additional parameters azd env set AZURE_SUBSCRIPTION_ID="${{ secrets.AZURE_SUBSCRIPTION_ID }}" - azd env set AZURE_ENV_OPENAI_LOCATION="${{ inputs.AZURE_ENV_OPENAI_LOCATION }}" + azd env set AZURE_ENV_OPENAI_LOCATION="${{ inputs.AZURE_LOCATION }}" azd env set AZURE_LOCATION="${{ inputs.AZURE_LOCATION }}" azd env set AZURE_RESOURCE_GROUP="${{ inputs.RESOURCE_GROUP_NAME }}" azd env set AZURE_ENV_IMAGETAG="${{ inputs.IMAGE_TAG }}" @@ -415,7 +415,7 @@ jobs: echo "| **Resource Group** | \`${{ inputs.RESOURCE_GROUP_NAME }}\` |" >> $GITHUB_STEP_SUMMARY echo "| **Configuration Type** | \`${{ inputs.WAF_ENABLED == 'true' && inputs.EXP == 'true' && 'WAF + EXP' || inputs.WAF_ENABLED == 'true' && inputs.EXP != 'true' && 'WAF + Non-EXP' || inputs.WAF_ENABLED != 'true' && inputs.EXP == 'true' && 'Non-WAF + EXP' || 'Non-WAF + Non-EXP' }}\` |" >> $GITHUB_STEP_SUMMARY echo "| **Azure Region (Infrastructure)** | \`${{ inputs.AZURE_LOCATION }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| **Azure OpenAI Region** | \`${{ inputs.AZURE_ENV_OPENAI_LOCATION }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Azure OpenAI Region** | \`${{ inputs.AZURE_LOCATION }}\` |" >> $GITHUB_STEP_SUMMARY echo "| **Docker Image Tag** | \`${{ inputs.IMAGE_TAG }}\` |" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY if [[ "${{ job.status }}" == "success" ]]; then From 2f2fdfd5bb34766f3944785672ff5cead4d8bfed Mon Sep 17 00:00:00 2001 From: Rafi-Microsoft Date: Tue, 16 Dec 2025 17:46:11 +0530 Subject: [PATCH 15/49] fix v5 --- .github/workflows/job-deploy-windows.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/job-deploy-windows.yml b/.github/workflows/job-deploy-windows.yml index 3a334525..6600b2a9 100644 --- a/.github/workflows/job-deploy-windows.yml +++ b/.github/workflows/job-deploy-windows.yml @@ -170,10 +170,10 @@ jobs: Write-Host "❌ EXP DISABLED - Skipping EXP parameters" } - # Deploy using azd up + # Deploy using azd up with inline parameter for aiDeploymentsLocation Write-Host "Starting deployment with azd up..." try { - azd up --no-prompt + azd up --no-prompt --parameter aiDeploymentsLocation="${{ inputs.AZURE_LOCATION }}" if ($LASTEXITCODE -ne 0) { Write-Host "❌ Deployment failed with exit code: $LASTEXITCODE" exit 1 From bfd953c936c49156ca50475f3c291a139b8c7089 Mon Sep 17 00:00:00 2001 From: Rafi-Microsoft Date: Wed, 17 Dec 2025 09:55:13 +0530 Subject: [PATCH 16/49] added open ai location param --- .github/workflows/job-deploy-windows.yml | 10 +++------- infra/main.parameters.json | 3 +++ infra/main.waf.parameters.json | 3 +++ 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/job-deploy-windows.yml b/.github/workflows/job-deploy-windows.yml index 6600b2a9..aadceeb0 100644 --- a/.github/workflows/job-deploy-windows.yml +++ b/.github/workflows/job-deploy-windows.yml @@ -128,15 +128,11 @@ jobs: # Set additional parameters azd env set AZURE_SUBSCRIPTION_ID="${{ secrets.AZURE_SUBSCRIPTION_ID }}" - azd env set AZURE_ENV_OPENAI_LOCATION="${{ inputs.AZURE_LOCATION }}" + azd env set AZURE_ENV_OPENAI_LOCATION="${{ inputs.AZURE_ENV_OPENAI_LOCATION }}" azd env set AZURE_LOCATION="${{ inputs.AZURE_LOCATION }}" azd env set AZURE_RESOURCE_GROUP="${{ inputs.RESOURCE_GROUP_NAME }}" azd env set AZURE_ENV_IMAGETAG="${{ inputs.IMAGE_TAG }}" - # Set infrastructure parameters that azd expects (both use AZURE_LOCATION like CI.yml) - azd env set aiDeploymentsLocation="${{ inputs.AZURE_LOCATION }}" - azd env set location="${{ inputs.AZURE_LOCATION }}" - # Set ACR name only when building Docker image if ("${{ inputs.BUILD_DOCKER_IMAGE }}" -eq "true") { $ACR_NAME = "${{ secrets.ACR_TEST_USERNAME }}" @@ -170,10 +166,10 @@ jobs: Write-Host "❌ EXP DISABLED - Skipping EXP parameters" } - # Deploy using azd up with inline parameter for aiDeploymentsLocation + # Deploy using azd up Write-Host "Starting deployment with azd up..." try { - azd up --no-prompt --parameter aiDeploymentsLocation="${{ inputs.AZURE_LOCATION }}" + azd up --no-prompt if ($LASTEXITCODE -ne 0) { Write-Host "❌ Deployment failed with exit code: $LASTEXITCODE" exit 1 diff --git a/infra/main.parameters.json b/infra/main.parameters.json index a649cdda..0e313833 100644 --- a/infra/main.parameters.json +++ b/infra/main.parameters.json @@ -8,6 +8,9 @@ "location": { "value": "${AZURE_LOCATION}" }, + "aiDeploymentsLocation": { + "value": "${AZURE_ENV_OPENAI_LOCATION}" + }, "gptModelDeploymentType": { "value": "${AZURE_ENV_MODEL_DEPLOYMENT_TYPE}" }, diff --git a/infra/main.waf.parameters.json b/infra/main.waf.parameters.json index 6700f98f..337be5fd 100644 --- a/infra/main.waf.parameters.json +++ b/infra/main.waf.parameters.json @@ -8,6 +8,9 @@ "location": { "value": "${AZURE_LOCATION}" }, + "aiDeploymentsLocation": { + "value": "${AZURE_ENV_OPENAI_LOCATION}" + }, "gptModelDeploymentType": { "value": "${AZURE_ENV_MODEL_DEPLOYMENT_TYPE}" }, From aa6108dad9173f2b19253b3d7cbe0b47fa0f0925 Mon Sep 17 00:00:00 2001 From: Rafi-Microsoft Date: Wed, 17 Dec 2025 12:07:36 +0530 Subject: [PATCH 17/49] added docker logs --- .github/workflows/job-deploy-windows.yml | 109 +++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/.github/workflows/job-deploy-windows.yml b/.github/workflows/job-deploy-windows.yml index aadceeb0..5e4d290e 100644 --- a/.github/workflows/job-deploy-windows.yml +++ b/.github/workflows/job-deploy-windows.yml @@ -91,6 +91,29 @@ jobs: with: driver: docker + - name: Verify Docker Installation + shell: pwsh + run: | + Write-Host "Verifying Docker installation..." + docker --version + docker info + Write-Host "✅ Docker is ready" + + - name: Login to Azure Container Registry + shell: pwsh + run: | + Write-Host "Pre-authenticating to ACR..." + # Note: Full ACR login will happen in resourcedeployment.ps1 + # This is just to verify ACR credentials are working + if ("${{ inputs.BUILD_DOCKER_IMAGE }}" -eq "true") { + $ACR_NAME = "${{ secrets.ACR_TEST_USERNAME }}" + Write-Host "ACR Name: $ACR_NAME" + az acr login --name $ACR_NAME + Write-Host "✅ ACR authentication successful" + } else { + Write-Host "Skipping ACR pre-authentication (using existing images)" + } + - name: Configure Parameters Based on WAF Setting shell: bash run: | @@ -222,14 +245,33 @@ jobs: - name: Run Deployment Script with Input shell: pwsh run: | + $ErrorActionPreference = "Stop" + + # Verify Docker is still running + Write-Host "Verifying Docker before deployment..." + docker ps + cd Deployment $input = @" ${{ secrets.EMAIL }} yes "@ + + Write-Host "Starting resourcedeployment.ps1..." $input | pwsh ./resourcedeployment.ps1 + + if ($LASTEXITCODE -ne 0) { + Write-Host "❌ resourcedeployment.ps1 failed with exit code: $LASTEXITCODE" + exit 1 + } + + Write-Host "✅ resourcedeployment.ps1 completed successfully" Write-Host "Resource Group Name is ${{ env.RESOURCE_GROUP_NAME }}" Write-Host "Kubernetes resource group is ${{ env.AZURE_AKS_NAME }}" + + # Verify pods are created + Write-Host "Checking pod status..." + kubectl get pods -n ns-km env: # From GitHub secrets (for login) AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} @@ -333,6 +375,73 @@ jobs: "WEB_APPURL=$WEB_APP_URL" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append "WEB_APPURL=$WEB_APP_URL" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + - name: Verify ACR Images + shell: bash + run: | + echo "🔍 Checking if Docker images exist in ACR..." + ACR_NAME="${{ env.AZURE_CONTAINER_REGISTRY_NAME }}" + + echo "Listing all repositories in ACR: $ACR_NAME" + az acr repository list --name "$ACR_NAME" --output table || echo "No repositories found" + + echo "" + echo "Checking for required images (kmgs namespace)..." + for repo in aiservice kernelmemory frontapp; do + echo "Checking kmgs/$repo..." + tags=$(az acr repository show-tags --name "$ACR_NAME" --repository "kmgs/$repo" --output table 2>/dev/null || echo "NOT FOUND") + if [ "$tags" = "NOT FOUND" ]; then + echo "❌ Image kmgs/$repo not found in ACR!" + else + echo "✅ Found tags: $tags" + fi + done + + - name: Check Pod Status and Logs + shell: bash + run: | + echo "🔍 Checking Kubernetes pod status..." + kubectl get pods -n ns-km -o wide + + echo "" + echo "📊 Checking pod events..." + kubectl get events -n ns-km --sort-by='.lastTimestamp' | tail -20 + + # Check if any pods are in ImagePullBackOff or Error state + failed_pods=$(kubectl get pods -n ns-km -o json | jq -r '.items[] | select(.status.phase != "Running") | .metadata.name') + + if [ -n "$failed_pods" ]; then + echo "⚠️ Found pods not in Running state:" + echo "$failed_pods" + + # Describe each failed pod for detailed error information + for pod in $failed_pods; do + echo "" + echo "📋 Describing pod: $pod" + kubectl describe pod "$pod" -n ns-km | tail -30 + + echo "" + echo "📄 Checking pod logs (if available):" + kubectl logs "$pod" -n ns-km --tail=50 || echo "No logs available yet" + done + + # Check if ImagePullBackOff is the issue + image_pull_errors=$(kubectl get pods -n ns-km -o json | jq -r '.items[] | select(.status.containerStatuses[].state.waiting.reason == "ImagePullBackOff") | .metadata.name') + + if [ -n "$image_pull_errors" ]; then + echo "" + echo "❌ ERROR: Pods are failing to pull Docker images!" + echo "This usually means:" + echo "1. Docker images weren't built/pushed to ACR" + echo "2. AKS doesn't have permission to pull from ACR" + echo "3. Image tags are incorrect" + echo "" + echo "Failing pods: $image_pull_errors" + exit 1 + fi + else + echo "✅ All pods are running successfully" + fi + - name: Validate Deployment shell: bash run: | From 910ebc3722078a5d600472210bf735f1d1ccc7d6 Mon Sep 17 00:00:00 2001 From: Rafi-Microsoft Date: Wed, 17 Dec 2025 12:46:27 +0530 Subject: [PATCH 18/49] docker issue fix v1 --- .github/workflows/job-deploy-windows.yml | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/.github/workflows/job-deploy-windows.yml b/.github/workflows/job-deploy-windows.yml index 5e4d290e..bfcec53d 100644 --- a/.github/workflows/job-deploy-windows.yml +++ b/.github/workflows/job-deploy-windows.yml @@ -41,7 +41,7 @@ on: jobs: deploy-windows: - runs-on: windows-latest + runs-on: ubuntu-latest env: AZURE_DEV_COLLECT_TELEMETRY: ${{ vars.AZURE_DEV_COLLECT_TELEMETRY }} outputs: @@ -92,27 +92,12 @@ jobs: driver: docker - name: Verify Docker Installation - shell: pwsh + shell: bash run: | - Write-Host "Verifying Docker installation..." + echo "Verifying Docker installation..." docker --version docker info - Write-Host "✅ Docker is ready" - - - name: Login to Azure Container Registry - shell: pwsh - run: | - Write-Host "Pre-authenticating to ACR..." - # Note: Full ACR login will happen in resourcedeployment.ps1 - # This is just to verify ACR credentials are working - if ("${{ inputs.BUILD_DOCKER_IMAGE }}" -eq "true") { - $ACR_NAME = "${{ secrets.ACR_TEST_USERNAME }}" - Write-Host "ACR Name: $ACR_NAME" - az acr login --name $ACR_NAME - Write-Host "✅ ACR authentication successful" - } else { - Write-Host "Skipping ACR pre-authentication (using existing images)" - } + echo "✅ Docker is ready" - name: Configure Parameters Based on WAF Setting shell: bash From cb4cfd1710e56071014f7f9873a6994322a3a8d1 Mon Sep 17 00:00:00 2001 From: Rafi-Microsoft Date: Wed, 17 Dec 2025 14:40:41 +0530 Subject: [PATCH 19/49] post dep fix v1 --- .github/workflows/job-deploy-windows.yml | 11 +++++++---- .github/workflows/job-deploy.yml | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/job-deploy-windows.yml b/.github/workflows/job-deploy-windows.yml index bfcec53d..ac915b99 100644 --- a/.github/workflows/job-deploy-windows.yml +++ b/.github/workflows/job-deploy-windows.yml @@ -467,6 +467,7 @@ jobs: fi - name: Run Post Deployment Script + continue-on-error: true shell: pwsh run: | Write-Host "Running post deployment script to upload files..." @@ -484,13 +485,15 @@ jobs: if ($LASTEXITCODE -eq $null -or $LASTEXITCODE -eq 0) { Write-Host "✅ Post deployment script completed successfully." } else { - Write-Host "❌ Post deployment script failed with exit code: $LASTEXITCODE" - exit 1 + Write-Host "⚠️ Post deployment script failed with exit code: $LASTEXITCODE" + Write-Host "⚠️ This is non-critical - deployment will continue" + exit 0 } } catch { - Write-Host "❌ Post deployment script failed with error: $($_.Exception.Message)" - exit 1 + Write-Host "⚠️ Post deployment script failed with error: $($_.Exception.Message)" + Write-Host "⚠️ This is non-critical - deployment will continue" + exit 0 } - name: Generate Deploy Job Summary diff --git a/.github/workflows/job-deploy.yml b/.github/workflows/job-deploy.yml index af2e534e..bc422bc1 100644 --- a/.github/workflows/job-deploy.yml +++ b/.github/workflows/job-deploy.yml @@ -217,7 +217,7 @@ jobs: echo "RESOURCE_GROUP_NAME=${{ inputs.resource_group_name }}" >> $GITHUB_ENV else echo "Generating a unique resource group name..." - ACCL_NAME="docgenv2" # Account name as specified + ACCL_NAME="dkmv2" # Account name as specified SHORT_UUID=$(uuidgen | cut -d'-' -f1) UNIQUE_RG_NAME="arg-${ACCL_NAME}-${SHORT_UUID}" echo "RESOURCE_GROUP_NAME=${UNIQUE_RG_NAME}" >> $GITHUB_ENV From 34eeea2e874bc6e9c93f0b44495ef5ef629edaf3 Mon Sep 17 00:00:00 2001 From: Rafi-Microsoft Date: Wed, 17 Dec 2025 16:50:28 +0530 Subject: [PATCH 20/49] fix v3 --- .github/workflows/job-deploy-windows.yml | 347 ++++------------------- 1 file changed, 61 insertions(+), 286 deletions(-) diff --git a/.github/workflows/job-deploy-windows.yml b/.github/workflows/job-deploy-windows.yml index ac915b99..e40a6222 100644 --- a/.github/workflows/job-deploy-windows.yml +++ b/.github/workflows/job-deploy-windows.yml @@ -45,11 +45,17 @@ jobs: env: AZURE_DEV_COLLECT_TELEMETRY: ${{ vars.AZURE_DEV_COLLECT_TELEMETRY }} outputs: - WEB_APPURL: ${{ steps.get_output_windows.outputs.WEB_APPURL }} + WEB_APPURL: ${{ steps.get_webapp_url.outputs.WEB_APPURL }} steps: - name: Checkout Code uses: actions/checkout@v4 + - name: Install Azure CLI + shell: bash + run: | + curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash + az --version # Verify installation + - name: Install Kubernetes CLI (kubectl) shell: bash run: | @@ -91,172 +97,69 @@ jobs: with: driver: docker - - name: Verify Docker Installation - shell: bash - run: | - echo "Verifying Docker installation..." - docker --version - docker info - echo "✅ Docker is ready" - - - name: Configure Parameters Based on WAF Setting - shell: bash - run: | - if [[ "${{ inputs.WAF_ENABLED }}" == "true" ]]; then - cp infra/main.waf.parameters.json infra/main.parameters.json - echo "✅ Successfully copied WAF parameters to main parameters file" - else - echo "🔧 Configuring Non-WAF deployment - using default main.parameters.json..." - fi - - - name: Setup Azure Developer CLI (Windows) + - name: Setup Azure Developer CLI uses: Azure/setup-azd@v2 - - name: Login to AZD - id: login-azure - shell: bash + - name: Login to Azure run: | az login --service-principal -u ${{ secrets.AZURE_CLIENT_ID }} -p ${{ secrets.AZURE_CLIENT_SECRET }} --tenant ${{ secrets.AZURE_TENANT_ID }} az account set --subscription ${{ secrets.AZURE_SUBSCRIPTION_ID }} azd auth login --client-id ${{ secrets.AZURE_CLIENT_ID }} --client-secret ${{ secrets.AZURE_CLIENT_SECRET }} --tenant-id ${{ secrets.AZURE_TENANT_ID }} - - name: Deploy using azd up and extract values (Windows) - id: get_output_windows - shell: pwsh + - name: Deploy using azd up + id: azd_deploy run: | - $ErrorActionPreference = "Stop" - - Write-Host "Creating environment..." + # Create azd environment azd env new ${{ inputs.ENV_NAME }} --no-prompt - Write-Host "Environment created: ${{ inputs.ENV_NAME }}" - - Write-Host "Setting default subscription..." + + # Set environment variables azd config set defaults.subscription ${{ secrets.AZURE_SUBSCRIPTION_ID }} - - # Set additional parameters azd env set AZURE_SUBSCRIPTION_ID="${{ secrets.AZURE_SUBSCRIPTION_ID }}" azd env set AZURE_ENV_OPENAI_LOCATION="${{ inputs.AZURE_ENV_OPENAI_LOCATION }}" azd env set AZURE_LOCATION="${{ inputs.AZURE_LOCATION }}" azd env set AZURE_RESOURCE_GROUP="${{ inputs.RESOURCE_GROUP_NAME }}" azd env set AZURE_ENV_IMAGETAG="${{ inputs.IMAGE_TAG }}" - # Set ACR name only when building Docker image - if ("${{ inputs.BUILD_DOCKER_IMAGE }}" -eq "true") { - $ACR_NAME = "${{ secrets.ACR_TEST_USERNAME }}" - azd env set AZURE_ENV_ACR_NAME="$ACR_NAME" - Write-Host "Set ACR name to: $ACR_NAME" - } else { - Write-Host "Skipping ACR name configuration (using existing image)" - } - - if ("${{ inputs.EXP }}" -eq "true") { - Write-Host "✅ EXP ENABLED - Setting EXP parameters..." - - # Set EXP variables dynamically - if ("${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}" -ne "") { - $EXP_LOG_ANALYTICS_ID = "${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}" - } else { - $EXP_LOG_ANALYTICS_ID = "${{ secrets.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}" - } - - if ("${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }}" -ne "") { - $EXP_AI_PROJECT_ID = "${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }}" - } else { - $EXP_AI_PROJECT_ID = "${{ secrets.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }}" - } - - Write-Host "AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: $EXP_LOG_ANALYTICS_ID" - Write-Host "AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: $EXP_AI_PROJECT_ID" - azd env set AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID="$EXP_LOG_ANALYTICS_ID" - azd env set AZURE_EXISTING_AI_PROJECT_RESOURCE_ID="$EXP_AI_PROJECT_ID" - } else { - Write-Host "❌ EXP DISABLED - Skipping EXP parameters" - } - - # Deploy using azd up - Write-Host "Starting deployment with azd up..." - try { - azd up --no-prompt - if ($LASTEXITCODE -ne 0) { - Write-Host "❌ Deployment failed with exit code: $LASTEXITCODE" - exit 1 - } - Write-Host "✅ Deployment succeeded." - } catch { - Write-Host "❌ Deployment failed with error: $($_.Exception.Message)" - exit 1 - } - - # Get deployment outputs using azd - Write-Host "Extracting deployment outputs..." - $DEPLOY_OUTPUT = azd env get-values --output json | ConvertFrom-Json - Write-Host "Deployment output: $($DEPLOY_OUTPUT | ConvertTo-Json -Depth 10)" + # Deploy + azd up --no-prompt - if (-not $DEPLOY_OUTPUT) { - Write-Host "❌ Error: Deployment output is empty. Please check the deployment logs." - exit 1 - } - - # Save all deployment outputs to GITHUB_ENV - Write-Host "Saving deployment outputs to environment variables..." - "RESOURCE_GROUP_NAME=$($DEPLOY_OUTPUT.RESOURCE_GROUP_NAME)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - "AZURE_RESOURCE_GROUP_ID=$($DEPLOY_OUTPUT.AZURE_RESOURCE_GROUP_ID)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - "STORAGE_ACCOUNT_NAME=$($DEPLOY_OUTPUT.STORAGE_ACCOUNT_NAME)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - "AZURE_SEARCH_SERVICE_NAME=$($DEPLOY_OUTPUT.AZURE_SEARCH_SERVICE_NAME)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - "AZURE_AKS_NAME=$($DEPLOY_OUTPUT.AZURE_AKS_NAME)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - "AZURE_AKS_MI_ID=$($DEPLOY_OUTPUT.AZURE_AKS_MI_ID)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - "AZURE_CONTAINER_REGISTRY_NAME=$($DEPLOY_OUTPUT.AZURE_CONTAINER_REGISTRY_NAME)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - "AZURE_COGNITIVE_SERVICE_NAME=$($DEPLOY_OUTPUT.AZURE_COGNITIVE_SERVICE_NAME)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - "AZURE_COGNITIVE_SERVICE_ENDPOINT=$($DEPLOY_OUTPUT.AZURE_COGNITIVE_SERVICE_ENDPOINT)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - "AZURE_OPENAI_SERVICE_NAME=$($DEPLOY_OUTPUT.AZURE_OPENAI_SERVICE_NAME)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - "AZURE_OPENAI_SERVICE_ENDPOINT=$($DEPLOY_OUTPUT.AZURE_OPENAI_SERVICE_ENDPOINT)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - "AZURE_COSMOSDB_NAME=$($DEPLOY_OUTPUT.AZURE_COSMOSDB_NAME)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - "AZ_GPT4O_MODEL_NAME=$($DEPLOY_OUTPUT.AZ_GPT4O_MODEL_NAME)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - "AZ_GPT4O_MODEL_ID=$($DEPLOY_OUTPUT.AZ_GPT4O_MODEL_ID)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - "AZ_GPT_EMBEDDING_MODEL_NAME=$($DEPLOY_OUTPUT.AZ_GPT_EMBEDDING_MODEL_NAME)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - "AZ_GPT_EMBEDDING_MODEL_ID=$($DEPLOY_OUTPUT.AZ_GPT_EMBEDDING_MODEL_ID)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - "AZURE_APP_CONFIG_ENDPOINT=$($DEPLOY_OUTPUT.AZURE_APP_CONFIG_ENDPOINT)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - "AZURE_APP_CONFIG_NAME=$($DEPLOY_OUTPUT.AZURE_APP_CONFIG_NAME)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + echo "✅ azd deployment completed" - # Get AKS managed resource group (node resource group) and save for later use - Write-Host "Retrieving AKS managed resource group..." - $AKS_NAME = $DEPLOY_OUTPUT.AZURE_AKS_NAME - $RESOURCE_GROUP = $DEPLOY_OUTPUT.RESOURCE_GROUP_NAME - $AKS_NODE_RG = az aks show --name $AKS_NAME --resource-group $RESOURCE_GROUP --query "nodeResourceGroup" -o tsv - Write-Host "AKS node resource group: $AKS_NODE_RG" - "krg_name=$AKS_NODE_RG" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + - name: Get Deployment Outputs + id: get_output + run: | + # Get outputs from azd + azd env get-values --output json > /tmp/azd_output.json + cat /tmp/azd_output.json + + # Extract values and write to GITHUB_ENV using bash + while IFS='=' read -r key value; do + # Remove quotes from value + value=$(echo "$value" | tr -d '"') + echo "${key}=${value}" >> $GITHUB_ENV + done < <(jq -r 'to_entries[] | "\(.key)=\(.value)"' /tmp/azd_output.json) + + # Get AKS node resource group if AKS exists + if [ -n "$AZURE_AKS_NAME" ]; then + krg_name=$(az aks show --name "$AZURE_AKS_NAME" --resource-group "$RESOURCE_GROUP_NAME" --query "nodeResourceGroup" -o tsv || echo "") + if [ -n "$krg_name" ]; then + echo "krg_name=$krg_name" >> $GITHUB_ENV + echo "AKS node resource group: $krg_name" + fi + fi - name: Run Deployment Script with Input shell: pwsh run: | - $ErrorActionPreference = "Stop" - - # Verify Docker is still running - Write-Host "Verifying Docker before deployment..." - docker ps - cd Deployment $input = @" ${{ secrets.EMAIL }} yes "@ - - Write-Host "Starting resourcedeployment.ps1..." $input | pwsh ./resourcedeployment.ps1 - - if ($LASTEXITCODE -ne 0) { - Write-Host "❌ resourcedeployment.ps1 failed with exit code: $LASTEXITCODE" - exit 1 - } - - Write-Host "✅ resourcedeployment.ps1 completed successfully" Write-Host "Resource Group Name is ${{ env.RESOURCE_GROUP_NAME }}" Write-Host "Kubernetes resource group is ${{ env.AZURE_AKS_NAME }}" - - # Verify pods are created - Write-Host "Checking pod status..." - kubectl get pods -n ns-km env: # From GitHub secrets (for login) AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} @@ -286,151 +189,32 @@ jobs: - name: Retrieve Web App URL id: get_webapp_url - shell: pwsh - run: | - Write-Host "Retrieving Web App URL from AKS..." - Write-Host "Kubernetes resource group: $env:krg_name" - - # Get Web App URL from AKS public IP with retry logic - $maxAttempts = 12 - $attempt = 1 - $PUBLIC_IP_NAME = $null - - while ($attempt -le $maxAttempts) { - Write-Host "Attempt $attempt/$maxAttempts : Checking for public IP..." - - # List all public IPs in the resource group for debugging - $allIPs = az network public-ip list --resource-group $env:krg_name --query "[].name" -o tsv - if ($allIPs) { - Write-Host "Available public IPs: $($allIPs -join ', ')" - } else { - Write-Host "No public IPs found yet in resource group $env:krg_name" - } - - # Try to find kubernetes public IP - $PUBLIC_IP_NAME = az network public-ip list --resource-group $env:krg_name --query "[?contains(name, 'kubernetes-')].name" -o tsv | Select-Object -First 1 - - if (-not [string]::IsNullOrWhiteSpace($PUBLIC_IP_NAME)) { - Write-Host "✅ Found public IP: $PUBLIC_IP_NAME" - break - } - - if ($attempt -lt $maxAttempts) { - Write-Host "Public IP not found yet, waiting 30 seconds..." - Start-Sleep -Seconds 30 - } - $attempt++ - } - - if ([string]::IsNullOrWhiteSpace($PUBLIC_IP_NAME)) { - Write-Host "❌ Error: Could not find public IP after $maxAttempts attempts in resource group $env:krg_name" - exit 1 - } - - # Get FQDN with retry - Write-Host "Retrieving FQDN for public IP: $PUBLIC_IP_NAME" - $FQDN = $null - $fqdnAttempts = 5 - $fqdnAttempt = 1 - - while ($fqdnAttempt -le $fqdnAttempts) { - Write-Host "Attempt $fqdnAttempt/$fqdnAttempts : Getting FQDN..." - $FQDN = az network public-ip show --resource-group $env:krg_name --name $PUBLIC_IP_NAME --query "dnsSettings.fqdn" -o tsv - - if (-not [string]::IsNullOrWhiteSpace($FQDN)) { - Write-Host "✅ Found FQDN: $FQDN" - break - } - - if ($fqdnAttempt -lt $fqdnAttempts) { - Write-Host "FQDN not available yet, waiting 15 seconds..." - Start-Sleep -Seconds 15 - } - $fqdnAttempt++ - } - - if ([string]::IsNullOrWhiteSpace($FQDN)) { - Write-Host "❌ Error: Could not retrieve FQDN for public IP $PUBLIC_IP_NAME after $fqdnAttempts attempts" - exit 1 - } - - $WEB_APP_URL = "https://$FQDN" - Write-Host "✅ Web App URL: $WEB_APP_URL" - - "WEB_APPURL=$WEB_APP_URL" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - "WEB_APPURL=$WEB_APP_URL" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append - - - name: Verify ACR Images shell: bash run: | - echo "🔍 Checking if Docker images exist in ACR..." - ACR_NAME="${{ env.AZURE_CONTAINER_REGISTRY_NAME }}" - - echo "Listing all repositories in ACR: $ACR_NAME" - az acr repository list --name "$ACR_NAME" --output table || echo "No repositories found" - - echo "" - echo "Checking for required images (kmgs namespace)..." - for repo in aiservice kernelmemory frontapp; do - echo "Checking kmgs/$repo..." - tags=$(az acr repository show-tags --name "$ACR_NAME" --repository "kmgs/$repo" --output table 2>/dev/null || echo "NOT FOUND") - if [ "$tags" = "NOT FOUND" ]; then - echo "❌ Image kmgs/$repo not found in ACR!" - else - echo "✅ Found tags: $tags" - fi - done + if az account show &> /dev/null; then + echo "Azure CLI is authenticated." + else + echo "Azure CLI is not authenticated. Logging in..." + az login --service-principal --username ${{ secrets.AZURE_CLIENT_ID }} --password ${{ secrets.AZURE_CLIENT_SECRET }} --tenant ${{ secrets.AZURE_TENANT_ID }} + fi + az account set --subscription ${{ secrets.AZURE_SUBSCRIPTION_ID }} - - name: Check Pod Status and Logs - shell: bash - run: | - echo "🔍 Checking Kubernetes pod status..." - kubectl get pods -n ns-km -o wide - - echo "" - echo "📊 Checking pod events..." - kubectl get events -n ns-km --sort-by='.lastTimestamp' | tail -20 - - # Check if any pods are in ImagePullBackOff or Error state - failed_pods=$(kubectl get pods -n ns-km -o json | jq -r '.items[] | select(.status.phase != "Running") | .metadata.name') - - if [ -n "$failed_pods" ]; then - echo "⚠️ Found pods not in Running state:" - echo "$failed_pods" - - # Describe each failed pod for detailed error information - for pod in $failed_pods; do - echo "" - echo "📋 Describing pod: $pod" - kubectl describe pod "$pod" -n ns-km | tail -30 - - echo "" - echo "📄 Checking pod logs (if available):" - kubectl logs "$pod" -n ns-km --tail=50 || echo "No logs available yet" - done - - # Check if ImagePullBackOff is the issue - image_pull_errors=$(kubectl get pods -n ns-km -o json | jq -r '.items[] | select(.status.containerStatuses[].state.waiting.reason == "ImagePullBackOff") | .metadata.name') - - if [ -n "$image_pull_errors" ]; then - echo "" - echo "❌ ERROR: Pods are failing to pull Docker images!" - echo "This usually means:" - echo "1. Docker images weren't built/pushed to ACR" - echo "2. AKS doesn't have permission to pull from ACR" - echo "3. Image tags are incorrect" - echo "" - echo "Failing pods: $image_pull_errors" - exit 1 - fi + # Get the Web App URL and save it to GITHUB_OUTPUT + echo "Retrieving Web App URL..." + public_ip_name=$(az network public-ip list --resource-group ${{ env.krg_name }} --query "[?contains(name, 'kubernetes-')].name" -o tsv) + fqdn=$(az network public-ip show --resource-group ${{ env.krg_name }} --name $public_ip_name --query "dnsSettings.fqdn" -o tsv) + if [ -n "$fqdn" ]; then + echo "WEB_APPURL=https://$fqdn" >> $GITHUB_OUTPUT + echo "Web App URL is https://$fqdn" else - echo "✅ All pods are running successfully" + echo "Failed to retrieve Web App URL." + exit 1 fi - name: Validate Deployment shell: bash run: | - webapp_url="${{ env.WEB_APPURL }}" + webapp_url="${{ steps.get_webapp_url.outputs.WEB_APPURL }}" echo "Validating web app at: $webapp_url" # Enhanced health check with retry logic @@ -471,29 +255,20 @@ jobs: shell: pwsh run: | Write-Host "Running post deployment script to upload files..." - Write-Host "WEB_APPURL: $env:WEB_APPURL" - - if ([string]::IsNullOrWhiteSpace($env:WEB_APPURL)) { - Write-Host "❌ Error: WEB_APPURL is empty" - exit 1 - } - cd Deployment try { - .\uploadfiles.ps1 -EndpointUrl $env:WEB_APPURL + .\uploadfiles.ps1 -EndpointUrl ${{ steps.get_webapp_url.outputs.WEB_APPURL }} Write-Host "ExitCode: $LASTEXITCODE" if ($LASTEXITCODE -eq $null -or $LASTEXITCODE -eq 0) { Write-Host "✅ Post deployment script completed successfully." } else { - Write-Host "⚠️ Post deployment script failed with exit code: $LASTEXITCODE" - Write-Host "⚠️ This is non-critical - deployment will continue" - exit 0 + Write-Host "❌ Post deployment script failed with exit code: $LASTEXITCODE" + exit 1 } } catch { - Write-Host "⚠️ Post deployment script failed with error: $($_.Exception.Message)" - Write-Host "⚠️ This is non-critical - deployment will continue" - exit 0 + Write-Host "❌ Post deployment script failed with error: $($_.Exception.Message)" + exit 1 } - name: Generate Deploy Job Summary From 7750aa45224341b11a34b181b29bc56aa68b292c Mon Sep 17 00:00:00 2001 From: Rafi-Microsoft Date: Thu, 18 Dec 2025 11:15:08 +0530 Subject: [PATCH 21/49] updated tokens --- .github/workflows/job-deploy-windows.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/job-deploy-windows.yml b/.github/workflows/job-deploy-windows.yml index e40a6222..e2e3835b 100644 --- a/.github/workflows/job-deploy-windows.yml +++ b/.github/workflows/job-deploy-windows.yml @@ -121,6 +121,10 @@ jobs: azd env set AZURE_RESOURCE_GROUP="${{ inputs.RESOURCE_GROUP_NAME }}" azd env set AZURE_ENV_IMAGETAG="${{ inputs.IMAGE_TAG }}" + # Set AI model capacity parameters + azd env set AZURE_ENV_MODEL_CAPACITY="150" + azd env set AZURE_ENV_EMBEDDING_MODEL_CAPACITY="200" + # Deploy azd up --no-prompt From da53491f23c94e9537bcb0f955f1a2b635a4f205 Mon Sep 17 00:00:00 2001 From: Rafi-Microsoft Date: Thu, 18 Dec 2025 13:42:42 +0530 Subject: [PATCH 22/49] added exp and waf support --- .github/workflows/job-deploy-windows.yml | 26 ++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/.github/workflows/job-deploy-windows.yml b/.github/workflows/job-deploy-windows.yml index e2e3835b..533b4486 100644 --- a/.github/workflows/job-deploy-windows.yml +++ b/.github/workflows/job-deploy-windows.yml @@ -50,6 +50,16 @@ jobs: - name: Checkout Code uses: actions/checkout@v4 + - name: Configure Parameters Based on WAF Setting + shell: bash + run: | + if [[ "${{ inputs.WAF_ENABLED }}" == "true" ]]; then + cp infra/main.waf.parameters.json infra/main.parameters.json + echo "✅ Successfully copied WAF parameters to main parameters file" + else + echo "🔧 Configuring Non-WAF deployment - using default main.parameters.json..." + fi + - name: Install Azure CLI shell: bash run: | @@ -124,6 +134,22 @@ jobs: # Set AI model capacity parameters azd env set AZURE_ENV_MODEL_CAPACITY="150" azd env set AZURE_ENV_EMBEDDING_MODEL_CAPACITY="200" + + if ("${{ inputs.EXP }}" -eq "true") { + Write-Host "✅ EXP ENABLED - Setting EXP parameters..." + + # Set EXP variables dynamically + if ("${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}" -ne "") { + $EXP_LOG_ANALYTICS_ID = "${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}" + } else { + $EXP_LOG_ANALYTICS_ID = "${{ secrets.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}" + } + + Write-Host "AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: $EXP_LOG_ANALYTICS_ID" + azd env set AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID="$EXP_LOG_ANALYTICS_ID" + } else { + Write-Host "❌ EXP DISABLED - Skipping EXP parameters" + } # Deploy azd up --no-prompt From 59fb40edb8c733a323cca23676ea4ef7166b550b Mon Sep 17 00:00:00 2001 From: Rafi-Microsoft Date: Thu, 18 Dec 2025 13:54:28 +0530 Subject: [PATCH 23/49] fix v1 --- .github/workflows/job-deploy-windows.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/job-deploy-windows.yml b/.github/workflows/job-deploy-windows.yml index 533b4486..94156674 100644 --- a/.github/workflows/job-deploy-windows.yml +++ b/.github/workflows/job-deploy-windows.yml @@ -119,6 +119,7 @@ jobs: - name: Deploy using azd up id: azd_deploy + shell: pwsh run: | # Create azd environment azd env new ${{ inputs.ENV_NAME }} --no-prompt From 601b650b529a3383c944ec0a7b1308eb5a9aeddc Mon Sep 17 00:00:00 2001 From: Rafi-Microsoft Date: Thu, 18 Dec 2025 14:24:33 +0530 Subject: [PATCH 24/49] code cleanup --- .github/workflows/deploy-orchestrator.yml | 6 ------ .github/workflows/deploy-windows.yml | 3 +-- .github/workflows/job-cleanup-deployment.yml | 6 +----- .github/workflows/job-deploy-windows.yml | 2 +- .github/workflows/job-deploy.yml | 11 +++-------- 5 files changed, 6 insertions(+), 22 deletions(-) diff --git a/.github/workflows/deploy-orchestrator.yml b/.github/workflows/deploy-orchestrator.yml index 9e99cec1..1eeb3875 100644 --- a/.github/workflows/deploy-orchestrator.yml +++ b/.github/workflows/deploy-orchestrator.yml @@ -3,10 +3,6 @@ name: Deployment orchestrator on: workflow_call: inputs: - runner_os: - description: 'Runner OS (ubuntu-latest or windows-latest)' - required: true - type: string azure_location: description: 'Azure Location For Deployment' required: false @@ -79,7 +75,6 @@ jobs: uses: ./.github/workflows/job-deploy.yml with: trigger_type: ${{ inputs.trigger_type }} - runner_os: ${{ inputs.runner_os }} azure_location: ${{ inputs.azure_location }} resource_group_name: ${{ inputs.resource_group_name }} waf_enabled: ${{ inputs.waf_enabled }} @@ -126,7 +121,6 @@ jobs: needs: [docker-build, deploy, e2e-test] uses: ./.github/workflows/job-cleanup-deployment.yml with: - runner_os: ${{ inputs.runner_os }} trigger_type: ${{ inputs.trigger_type }} cleanup_resources: ${{ inputs.cleanup_resources }} existing_webapp_url: ${{ inputs.existing_webapp_url }} diff --git a/.github/workflows/deploy-windows.yml b/.github/workflows/deploy-windows.yml index 2e96c838..795daae3 100644 --- a/.github/workflows/deploy-windows.yml +++ b/.github/workflows/deploy-windows.yml @@ -1,4 +1,4 @@ -name: Deploy-Test-Cleanup (v2) Windows +name: Deploy-Test-Cleanup (v2) on: push: branches: @@ -85,7 +85,6 @@ jobs: Run: uses: ./.github/workflows/deploy-orchestrator.yml with: - runner_os: windows-latest azure_location: ${{ github.event.inputs.azure_location || 'australiaeast' }} resource_group_name: ${{ github.event.inputs.resource_group_name || '' }} waf_enabled: ${{ github.event.inputs.waf_enabled == 'true' }} diff --git a/.github/workflows/job-cleanup-deployment.yml b/.github/workflows/job-cleanup-deployment.yml index 6b920a4e..d1e7d311 100644 --- a/.github/workflows/job-cleanup-deployment.yml +++ b/.github/workflows/job-cleanup-deployment.yml @@ -2,10 +2,6 @@ name: Cleanup Deployment Job on: workflow_call: inputs: - runner_os: - description: 'Runner OS (ubuntu-latest or windows-latest)' - required: true - type: string trigger_type: description: 'Trigger type (workflow_dispatch, pull_request, schedule)' required: true @@ -43,7 +39,7 @@ on: jobs: cleanup-deployment: - runs-on: ${{ inputs.runner_os }} + runs-on: ubuntu-latest continue-on-error: true env: RESOURCE_GROUP_NAME: ${{ inputs.RESOURCE_GROUP_NAME }} diff --git a/.github/workflows/job-deploy-windows.yml b/.github/workflows/job-deploy-windows.yml index 94156674..d1d618a3 100644 --- a/.github/workflows/job-deploy-windows.yml +++ b/.github/workflows/job-deploy-windows.yml @@ -1,4 +1,4 @@ -name: Deploy Steps - Windows +name: Deploy Steps on: workflow_call: diff --git a/.github/workflows/job-deploy.yml b/.github/workflows/job-deploy.yml index bc422bc1..0c40e35e 100644 --- a/.github/workflows/job-deploy.yml +++ b/.github/workflows/job-deploy.yml @@ -7,10 +7,6 @@ on: description: 'Trigger type (workflow_dispatch, pull_request, schedule)' required: true type: string - runner_os: - description: 'Runner OS (ubuntu-latest or windows-latest)' - required: true - type: string azure_location: description: 'Azure Location For Deployment' required: false @@ -315,7 +311,6 @@ jobs: echo "|---------------|-------|" >> $GITHUB_STEP_SUMMARY echo "| **Trigger Type** | \`${{ github.event_name }}\` |" >> $GITHUB_STEP_SUMMARY echo "| **Branch** | \`${{ env.BRANCH_NAME }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| **Runner OS** | \`${{ inputs.runner_os }}\` |" >> $GITHUB_STEP_SUMMARY echo "| **WAF Enabled** | ${{ env.WAF_ENABLED == 'true' && '✅ Yes' || '❌ No' }} |" >> $GITHUB_STEP_SUMMARY echo "| **EXP Enabled** | ${{ env.EXP == 'true' && '✅ Yes' || '❌ No' }} |" >> $GITHUB_STEP_SUMMARY echo "| **Run E2E Tests** | \`${{ env.RUN_E2E_TESTS }}\` |" >> $GITHUB_STEP_SUMMARY @@ -340,10 +335,10 @@ jobs: echo "ℹ️ **Note:** Manual Trigger - Using user-specified configuration" >> $GITHUB_STEP_SUMMARY fi - deploy-windows: - name: Deploy on Windows + deploy-linux: + name: Deploy needs: azure-setup - if: inputs.runner_os == 'windows-latest' && always() && needs.azure-setup.result == 'success' + if: "!Cancelled() && needs.azure-setup.result == 'success'" uses: ./.github/workflows/job-deploy-windows.yml with: ENV_NAME: ${{ needs.azure-setup.outputs.ENV_NAME }} From 5c3613a84758f277950db90c8416374612614813 Mon Sep 17 00:00:00 2001 From: Vamshi-Microsoft Date: Thu, 18 Dec 2025 15:24:37 +0530 Subject: [PATCH 25/49] removed SecurityControl tag --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 57c26bb6..58b6ab11 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -215,7 +215,7 @@ jobs: enableRedundancy=false \ enableScalability=false \ createdBy="Pipeline" \ - tags="{'SecurityControl':'Ignore','Purpose':'Deploying and Cleaning Up Resources for Validation','CreatedDate':'$current_date'}" + tags="{'Purpose':'Deploying and Cleaning Up Resources for Validation','CreatedDate':'$current_date'}" - name: Get Deployment Output and extract Values id: get_output From 1231dbbdc20954b21434da7efdd5e7e56e3df7b8 Mon Sep 17 00:00:00 2001 From: Rafi-Microsoft Date: Thu, 18 Dec 2025 16:09:19 +0530 Subject: [PATCH 26/49] code cleanup v2 --- .github/workflows/deploy-orchestrator.yml | 2 +- .github/workflows/job-deploy-windows.yml | 2 +- .github/workflows/job-send-notification.yml | 2 +- .github/workflows/test-automation-v2.yml | 10 +++++----- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/deploy-orchestrator.yml b/.github/workflows/deploy-orchestrator.yml index 1eeb3875..66d070e1 100644 --- a/.github/workflows/deploy-orchestrator.yml +++ b/.github/workflows/deploy-orchestrator.yml @@ -93,7 +93,7 @@ jobs: needs: [docker-build, deploy] uses: ./.github/workflows/test-automation-v2.yml with: - DOCGEN_URL: ${{ needs.deploy.outputs.WEB_APPURL || inputs.existing_webapp_url }} + Test_URL: ${{ needs.deploy.outputs.WEB_APPURL || inputs.existing_webapp_url }} TEST_SUITE: ${{ inputs.trigger_type == 'workflow_dispatch' && inputs.run_e2e_tests || 'GoldenPath-Testing' }} secrets: inherit diff --git a/.github/workflows/job-deploy-windows.yml b/.github/workflows/job-deploy-windows.yml index d1d618a3..45610392 100644 --- a/.github/workflows/job-deploy-windows.yml +++ b/.github/workflows/job-deploy-windows.yml @@ -319,7 +319,7 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY if [[ "${{ job.status }}" == "success" ]]; then echo "### ✅ Deployment Details" >> $GITHUB_STEP_SUMMARY - echo "- **Web App URL**: [${{ steps.get_output_windows.outputs.WEB_APPURL }}](${{ steps.get_output_windows.outputs.WEB_APPURL }})" >> $GITHUB_STEP_SUMMARY + echo "- **Web App URL**: [${{ steps.get_output.outputs.WEB_APPURL }}](${{ steps.get_output.outputs.WEB_APPURL }})" >> $GITHUB_STEP_SUMMARY echo "- Successfully deployed to Azure with all resources configured" >> $GITHUB_STEP_SUMMARY echo "- Post-deployment scripts executed successfully" >> $GITHUB_STEP_SUMMARY else diff --git a/.github/workflows/job-send-notification.yml b/.github/workflows/job-send-notification.yml index 87baad34..89189fa4 100644 --- a/.github/workflows/job-send-notification.yml +++ b/.github/workflows/job-send-notification.yml @@ -73,7 +73,7 @@ jobs: runs-on: ubuntu-latest continue-on-error: true env: - accelerator_name: "DocGen" + accelerator_name: "DKM" steps: - name: Determine Test Suite Display Name id: test_suite diff --git a/.github/workflows/test-automation-v2.yml b/.github/workflows/test-automation-v2.yml index 085693ba..23e9d611 100644 --- a/.github/workflows/test-automation-v2.yml +++ b/.github/workflows/test-automation-v2.yml @@ -1,12 +1,12 @@ -name: Test Automation DocGen-v2 +name: Test Automation Dkm-v2 on: workflow_call: inputs: - DOCGEN_URL: + Test_URL: required: true type: string - description: "Web URL for DocGen" + description: "Web URL for Dkm" TEST_SUITE: required: false type: string @@ -21,8 +21,8 @@ on: value: ${{ jobs.test.outputs.TEST_REPORT_URL }} env: - url: ${{ inputs.DOCGEN_URL }} - accelerator_name: "DocGen" + url: ${{ inputs.Test_URL }} + accelerator_name: "Dkm" test_suite: ${{ inputs.TEST_SUITE }} jobs: From 4b1fa16333e1441e84675bb7f25589bd50b1e24c Mon Sep 17 00:00:00 2001 From: Rafi-Microsoft Date: Thu, 18 Dec 2025 16:54:29 +0530 Subject: [PATCH 27/49] removed docker build file and code cleanup --- .github/workflows/deploy-orchestrator.yml | 27 +------ .github/workflows/deploy-windows.yml | 12 --- .github/workflows/job-deploy-windows.yml | 6 -- .github/workflows/job-deploy.yml | 50 +++--------- .github/workflows/job-docker-build.yml | 99 ----------------------- 5 files changed, 14 insertions(+), 180 deletions(-) delete mode 100644 .github/workflows/job-docker-build.yml diff --git a/.github/workflows/deploy-orchestrator.yml b/.github/workflows/deploy-orchestrator.yml index 66d070e1..880e94e6 100644 --- a/.github/workflows/deploy-orchestrator.yml +++ b/.github/workflows/deploy-orchestrator.yml @@ -23,11 +23,6 @@ on: required: false default: false type: boolean - build_docker_image: - description: 'Build And Push Docker Image (Optional)' - required: false - default: false - type: boolean cleanup_resources: description: 'Cleanup Deployed Resources' required: false @@ -43,11 +38,6 @@ on: required: false default: '' type: string - AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: - description: 'AI Project Resource ID (Optional)' - required: false - default: '' - type: string existing_webapp_url: description: 'Existing Container WebApp URL (Skips Deployment)' required: false @@ -62,16 +52,8 @@ env: AZURE_DEV_COLLECT_TELEMETRY: ${{ vars.AZURE_DEV_COLLECT_TELEMETRY }} jobs: - docker-build: - uses: ./.github/workflows/job-docker-build.yml - with: - trigger_type: ${{ inputs.trigger_type }} - build_docker_image: ${{ inputs.build_docker_image }} - secrets: inherit - deploy: if: always() && (inputs.trigger_type != 'workflow_dispatch' || inputs.existing_webapp_url == '' || inputs.existing_webapp_url == null) - needs: docker-build uses: ./.github/workflows/job-deploy.yml with: trigger_type: ${{ inputs.trigger_type }} @@ -79,18 +61,15 @@ jobs: resource_group_name: ${{ inputs.resource_group_name }} waf_enabled: ${{ inputs.waf_enabled }} EXP: ${{ inputs.EXP }} - build_docker_image: ${{ inputs.build_docker_image }} existing_webapp_url: ${{ inputs.existing_webapp_url }} AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }} - AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }} - docker_image_tag: ${{ needs.docker-build.outputs.IMAGE_TAG }} run_e2e_tests: ${{ inputs.run_e2e_tests }} cleanup_resources: ${{ inputs.cleanup_resources }} secrets: inherit e2e-test: if: always() && ((needs.deploy.result == 'success' && needs.deploy.outputs.WEB_APPURL != '') || (inputs.existing_webapp_url != '' && inputs.existing_webapp_url != null)) && (inputs.trigger_type != 'workflow_dispatch' || (inputs.run_e2e_tests != 'None' && inputs.run_e2e_tests != '' && inputs.run_e2e_tests != null)) - needs: [docker-build, deploy] + needs: [deploy] uses: ./.github/workflows/test-automation-v2.yml with: Test_URL: ${{ needs.deploy.outputs.WEB_APPURL || inputs.existing_webapp_url }} @@ -99,7 +78,7 @@ jobs: send-notification: if: always() - needs: [docker-build, deploy, e2e-test] + needs: [deploy, e2e-test] uses: ./.github/workflows/job-send-notification.yml with: trigger_type: ${{ inputs.trigger_type }} @@ -118,7 +97,7 @@ jobs: cleanup-deployment: if: always() && needs.deploy.result == 'success' && needs.deploy.outputs.RESOURCE_GROUP_NAME != '' && inputs.existing_webapp_url == '' && (inputs.trigger_type != 'workflow_dispatch' || inputs.cleanup_resources) - needs: [docker-build, deploy, e2e-test] + needs: [deploy, e2e-test] uses: ./.github/workflows/job-cleanup-deployment.yml with: trigger_type: ${{ inputs.trigger_type }} diff --git a/.github/workflows/deploy-windows.yml b/.github/workflows/deploy-windows.yml index 795daae3..7aff690b 100644 --- a/.github/workflows/deploy-windows.yml +++ b/.github/workflows/deploy-windows.yml @@ -40,11 +40,6 @@ on: required: false default: false type: boolean - build_docker_image: - description: 'Build And Push Docker Image (Optional)' - required: false - default: false - type: boolean cleanup_resources: description: 'Cleanup Deployed Resources' @@ -67,11 +62,6 @@ on: required: false default: '' type: string - AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: - description: 'AI Project Resource ID (Optional)' - required: false - default: '' - type: string existing_webapp_url: description: 'Existing WebApp URL (Skips Deployment)' required: false @@ -89,11 +79,9 @@ jobs: resource_group_name: ${{ github.event.inputs.resource_group_name || '' }} waf_enabled: ${{ github.event.inputs.waf_enabled == 'true' }} EXP: ${{ github.event.inputs.EXP == 'true' }} - build_docker_image: ${{ github.event.inputs.build_docker_image == 'true' }} cleanup_resources: ${{ github.event.inputs.cleanup_resources == 'true' }} run_e2e_tests: ${{ github.event.inputs.run_e2e_tests || 'GoldenPath-Testing' }} AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ github.event.inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID || '' }} - AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ github.event.inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID || '' }} existing_webapp_url: ${{ github.event.inputs.existing_webapp_url || '' }} trigger_type: ${{ github.event_name }} secrets: inherit diff --git a/.github/workflows/job-deploy-windows.yml b/.github/workflows/job-deploy-windows.yml index 45610392..8d8b6aac 100644 --- a/.github/workflows/job-deploy-windows.yml +++ b/.github/workflows/job-deploy-windows.yml @@ -18,9 +18,6 @@ on: IMAGE_TAG: required: true type: string - BUILD_DOCKER_IMAGE: - required: true - type: string EXP: required: true type: string @@ -31,9 +28,6 @@ on: AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: required: false type: string - AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: - required: false - type: string outputs: WEB_APPURL: description: "Container Web App URL" diff --git a/.github/workflows/job-deploy.yml b/.github/workflows/job-deploy.yml index 0c40e35e..b3500b45 100644 --- a/.github/workflows/job-deploy.yml +++ b/.github/workflows/job-deploy.yml @@ -27,11 +27,6 @@ on: required: false default: false type: boolean - build_docker_image: - description: 'Build And Push Docker Image (Optional)' - required: false - default: false - type: boolean cleanup_resources: description: 'Cleanup Deployed Resources' required: false @@ -52,16 +47,6 @@ on: required: false default: '' type: string - AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: - description: 'AI Project Resource ID (Optional)' - required: false - default: '' - type: string - docker_image_tag: - description: 'Docker Image Tag from build job' - required: false - default: '' - type: string outputs: RESOURCE_GROUP_NAME: description: "Resource Group Name" @@ -93,7 +78,7 @@ env: EXP: ${{ inputs.trigger_type == 'workflow_dispatch' && (inputs.EXP || false) || false }} CLEANUP_RESOURCES: ${{ inputs.trigger_type != 'workflow_dispatch' || inputs.cleanup_resources }} RUN_E2E_TESTS: ${{ inputs.trigger_type == 'workflow_dispatch' && (inputs.run_e2e_tests || 'GoldenPath-Testing') || 'GoldenPath-Testing' }} - BUILD_DOCKER_IMAGE: ${{ inputs.trigger_type == 'workflow_dispatch' && (inputs.build_docker_image || false) || false }} + jobs: azure-setup: @@ -115,12 +100,11 @@ jobs: echo "🔍 Validating EXP configuration..." if [[ "${{ inputs.EXP }}" != "true" ]]; then - if [[ -n "${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}" ]] || [[ -n "${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }}" ]]; then + if [[ -n "${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}" ]]; then echo "🔧 AUTO-ENABLING EXP: EXP parameter values were provided but EXP was not explicitly enabled." echo "" echo "You provided values for:" [[ -n "${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}" ]] && echo " - Azure Log Analytics Workspace ID: '${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}'" - [[ -n "${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }}" ]] && echo " - Azure AI Project Resource ID: '${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }}'" echo "" echo "✅ Automatically enabling EXP to use these values." echo "EXP=true" >> $GITHUB_ENV @@ -213,7 +197,7 @@ jobs: echo "RESOURCE_GROUP_NAME=${{ inputs.resource_group_name }}" >> $GITHUB_ENV else echo "Generating a unique resource group name..." - ACCL_NAME="dkmv2" # Account name as specified + ACCL_NAME="dkm" # Account name as specified SHORT_UUID=$(uuidgen | cut -d'-' -f1) UNIQUE_RG_NAME="arg-${ACCL_NAME}-${SHORT_UUID}" echo "RESOURCE_GROUP_NAME=${UNIQUE_RG_NAME}" >> $GITHUB_ENV @@ -256,23 +240,14 @@ jobs: - name: Determine Docker Image Tag id: determine_image_tag run: | - if [[ "${{ env.BUILD_DOCKER_IMAGE }}" == "true" ]]; then - if [[ -n "${{ inputs.docker_image_tag }}" ]]; then - IMAGE_TAG="${{ inputs.docker_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 }}" - echo "Current branch: $BRANCH_NAME" - - # Determine image tag based on branch - if [[ "$BRANCH_NAME" == "main" ]]; then - IMAGE_TAG="latest_waf" - echo "Using main branch - image tag: latest_waf" + echo "🏷️ Using existing Docker image based on branch..." + BRANCH_NAME="${{ env.BRANCH_NAME }}" + echo "Current branch: $BRANCH_NAME" + + # Determine image tag based on branch + if [[ "$BRANCH_NAME" == "main" ]]; then + IMAGE_TAG="latest_waf" + echo "Using main branch - image tag: latest_waf" elif [[ "$BRANCH_NAME" == "dev" ]]; then IMAGE_TAG="dev" echo "Using dev branch - image tag: dev" @@ -315,7 +290,6 @@ jobs: echo "| **EXP Enabled** | ${{ env.EXP == 'true' && '✅ Yes' || '❌ No' }} |" >> $GITHUB_STEP_SUMMARY echo "| **Run E2E Tests** | \`${{ env.RUN_E2E_TESTS }}\` |" >> $GITHUB_STEP_SUMMARY echo "| **Cleanup Resources** | ${{ env.CLEANUP_RESOURCES == 'true' && '✅ Yes' || '❌ No' }} |" >> $GITHUB_STEP_SUMMARY - echo "| **Build Docker Image** | ${{ env.BUILD_DOCKER_IMAGE == 'true' && '✅ Yes' || '❌ No' }} |" >> $GITHUB_STEP_SUMMARY if [[ "${{ inputs.trigger_type }}" == "workflow_dispatch" && -n "${{ inputs.azure_location }}" ]]; then echo "| **Azure Location** | \`${{ inputs.azure_location }}\` (User Selected) |" >> $GITHUB_STEP_SUMMARY @@ -346,9 +320,7 @@ jobs: AZURE_LOCATION: ${{ needs.azure-setup.outputs.AZURE_LOCATION }} RESOURCE_GROUP_NAME: ${{ needs.azure-setup.outputs.RESOURCE_GROUP_NAME }} IMAGE_TAG: ${{ needs.azure-setup.outputs.IMAGE_TAG }} - BUILD_DOCKER_IMAGE: ${{ inputs.build_docker_image || 'false' }} EXP: ${{ inputs.EXP || 'false' }} WAF_ENABLED: ${{ inputs.waf_enabled == true && 'true' || 'false' }} AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }} - AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }} secrets: inherit diff --git a/.github/workflows/job-docker-build.yml b/.github/workflows/job-docker-build.yml deleted file mode 100644 index 62956a43..00000000 --- a/.github/workflows/job-docker-build.yml +++ /dev/null @@ -1,99 +0,0 @@ -name: Docker Build Job - -on: - workflow_call: - inputs: - trigger_type: - description: 'Trigger type (workflow_dispatch, pull_request, schedule)' - required: true - type: string - build_docker_image: - description: 'Build And Push Docker Image (Optional)' - required: false - default: false - type: boolean - outputs: - IMAGE_TAG: - description: "Generated Docker Image Tag" - value: ${{ jobs.docker-build.outputs.IMAGE_TAG }} - -env: - BRANCH_NAME: ${{ github.event.workflow_run.head_branch || github.head_ref || github.ref_name }} - -jobs: - docker-build: - if: inputs.trigger_type == 'workflow_dispatch' && 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 - shell: bash - run: | - echo "🔨 Building new Docker image - generating unique tag..." - TIMESTAMP=$(date +%Y%m%d-%H%M%S) - RUN_ID="${{ github.run_id }}" - BRANCH_NAME="${{ github.head_ref || github.ref_name }}" - 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 - env: - DOCKER_BUILD_SUMMARY: false - 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 - shell: bash - run: | - echo "✅ Docker image successfully built and pushed" - echo "Image tag: ${{ steps.generate_docker_tag.outputs.IMAGE_TAG }}" - - - name: Generate Docker Build Summary - if: always() - shell: bash - run: | - ACR_NAME=$(echo "${{ secrets.ACR_TEST_LOGIN_SERVER }}") - echo "## 🐳 Docker Build Job Summary" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY - echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY - echo "| **Job Status** | ${{ job.status == 'success' && '✅ Success' || '❌ Failed' }} |" >> $GITHUB_STEP_SUMMARY - echo "| **Image Tag** | \`${{ steps.generate_docker_tag.outputs.IMAGE_TAG }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| **Branch** | ${{ env.BRANCH_NAME }} |" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - if [[ "${{ job.status }}" == "success" ]]; then - echo "### ✅ Build Details" >> $GITHUB_STEP_SUMMARY - echo "Successfully built and pushed one Docker image to ACR:" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Built Images:**" >> $GITHUB_STEP_SUMMARY - echo "- \`${ACR_NAME}.azurecr.io/webapp:${{ steps.generate_docker_tag.outputs.IMAGE_TAG }}\`" >> $GITHUB_STEP_SUMMARY - else - echo "### ❌ Build Failed" >> $GITHUB_STEP_SUMMARY - echo "- Docker build process encountered an error" >> $GITHUB_STEP_SUMMARY - echo "- Check the docker-build job for detailed error information" >> $GITHUB_STEP_SUMMARY - fi From bf2f244c28ba8aa33ab36b2554ab1813337b1bc9 Mon Sep 17 00:00:00 2001 From: Rafi-Microsoft Date: Thu, 18 Dec 2025 16:58:03 +0530 Subject: [PATCH 28/49] fix v1 --- .github/workflows/job-deploy.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/job-deploy.yml b/.github/workflows/job-deploy.yml index b3500b45..8f9c3d38 100644 --- a/.github/workflows/job-deploy.yml +++ b/.github/workflows/job-deploy.yml @@ -260,7 +260,6 @@ jobs: fi echo "Using existing Docker image tag: $IMAGE_TAG" - fi echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_OUTPUT From 0c282949d66f48847454b694333e61b280461a83 Mon Sep 17 00:00:00 2001 From: Rafi-Microsoft Date: Thu, 18 Dec 2025 17:53:44 +0530 Subject: [PATCH 29/49] e to e step fix --- .github/workflows/job-deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/job-deploy.yml b/.github/workflows/job-deploy.yml index 8f9c3d38..de052305 100644 --- a/.github/workflows/job-deploy.yml +++ b/.github/workflows/job-deploy.yml @@ -53,7 +53,7 @@ on: value: ${{ jobs.azure-setup.outputs.RESOURCE_GROUP_NAME }} WEB_APPURL: description: "Container Web App URL" - value: ${{ jobs.deploy-windows.outputs.WEB_APPURL }} + value: ${{ jobs.deploy-linux.outputs.WEB_APPURL }} ENV_NAME: description: "Environment Name" value: ${{ jobs.azure-setup.outputs.ENV_NAME }} From 54f9fe93b86b6b9f26271ac29e37a134927b8a91 Mon Sep 17 00:00:00 2001 From: Rafi-Microsoft Date: Thu, 18 Dec 2025 20:22:25 +0530 Subject: [PATCH 30/49] updated output params --- .github/workflows/job-deploy-windows.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/job-deploy-windows.yml b/.github/workflows/job-deploy-windows.yml index 8d8b6aac..646ca426 100644 --- a/.github/workflows/job-deploy-windows.yml +++ b/.github/workflows/job-deploy-windows.yml @@ -313,7 +313,7 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY if [[ "${{ job.status }}" == "success" ]]; then echo "### ✅ Deployment Details" >> $GITHUB_STEP_SUMMARY - echo "- **Web App URL**: [${{ steps.get_output.outputs.WEB_APPURL }}](${{ steps.get_output.outputs.WEB_APPURL }})" >> $GITHUB_STEP_SUMMARY + echo "- **Web App URL**: [${{ steps.get_webapp_url.outputs.WEB_APPURL }}](${{ steps.get_webapp_url.outputs.WEB_APPURL }})" >> $GITHUB_STEP_SUMMARY echo "- Successfully deployed to Azure with all resources configured" >> $GITHUB_STEP_SUMMARY echo "- Post-deployment scripts executed successfully" >> $GITHUB_STEP_SUMMARY else From e75ff5f991c12c306824a418769c7a385333d859 Mon Sep 17 00:00:00 2001 From: Rafi-Microsoft Date: Fri, 19 Dec 2025 11:58:46 +0530 Subject: [PATCH 31/49] added ai location param --- docs/CustomizingAzdParameters.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/CustomizingAzdParameters.md b/docs/CustomizingAzdParameters.md index 15625e6b..8756c99f 100644 --- a/docs/CustomizingAzdParameters.md +++ b/docs/CustomizingAzdParameters.md @@ -10,6 +10,7 @@ By default this template will use the environment name as the prefix to prevent | ------------------------------- | ------ | ----------------- | --------------------------------------------------------------------------------------------------- | | `AZURE_ENV_NAME` | string | `dkm` | Used as a prefix for all resource names to ensure uniqueness across environments. | | `AZURE_LOCATION` | string | `` | Location of the Azure resources. Controls where the infrastructure will be deployed. | +| `AZURE_ENV_OPENAI_LOCATION` | string | `` | Location for Azure OpenAI resources. Can be different from AZURE_LOCATION for optimized AI service placement. | | `AZURE_ENV_MODEL_DEPLOYMENT_TYPE` | string | `GlobalStandard` | Defines the deployment type for the AI model (e.g., Standard, GlobalStandard). | | `AZURE_ENV_MODEL_NAME` | string | `gpt-4.1` | Specifies the name of the GPT model to be deployed. | | `AZURE_ENV_MODEL_CAPACITY` | int | `100` | Sets the GPT model capacity. | From 8e85a0dbffe1e8f1bbb8251790bac4a0911fd22f Mon Sep 17 00:00:00 2001 From: Rafi-Microsoft Date: Mon, 22 Dec 2025 11:58:43 +0530 Subject: [PATCH 32/49] fixed copilot suggested fixes --- .../{deploy-windows.yml => deploy-linux.yml} | 3 -- .github/workflows/deploy-orchestrator.yml | 8 ++--- .github/workflows/job-cleanup-deployment.yml | 2 +- ...eploy-windows.yml => job-deploy-linux.yml} | 30 +++++++------------ .github/workflows/job-deploy.yml | 21 +++++++------ .github/workflows/test-automation-v2.yml | 8 +++-- 6 files changed, 32 insertions(+), 40 deletions(-) rename .github/workflows/{deploy-windows.yml => deploy-linux.yml} (96%) rename .github/workflows/{job-deploy-windows.yml => job-deploy-linux.yml} (91%) diff --git a/.github/workflows/deploy-windows.yml b/.github/workflows/deploy-linux.yml similarity index 96% rename from .github/workflows/deploy-windows.yml rename to .github/workflows/deploy-linux.yml index 7aff690b..7fd7462f 100644 --- a/.github/workflows/deploy-windows.yml +++ b/.github/workflows/deploy-linux.yml @@ -67,9 +67,6 @@ on: required: false default: '' type: string - - # schedule: - # - cron: '0 9,21 * * *' # Runs at 9:00 AM and 9:00 PM GMT jobs: Run: diff --git a/.github/workflows/deploy-orchestrator.yml b/.github/workflows/deploy-orchestrator.yml index 880e94e6..6f0de5d4 100644 --- a/.github/workflows/deploy-orchestrator.yml +++ b/.github/workflows/deploy-orchestrator.yml @@ -53,7 +53,7 @@ env: jobs: deploy: - if: always() && (inputs.trigger_type != 'workflow_dispatch' || inputs.existing_webapp_url == '' || inputs.existing_webapp_url == null) + if: "!cancelled() && (inputs.trigger_type != 'workflow_dispatch' || inputs.existing_webapp_url == '' || inputs.existing_webapp_url == null)" uses: ./.github/workflows/job-deploy.yml with: trigger_type: ${{ inputs.trigger_type }} @@ -68,7 +68,7 @@ jobs: secrets: inherit e2e-test: - if: always() && ((needs.deploy.result == 'success' && needs.deploy.outputs.WEB_APPURL != '') || (inputs.existing_webapp_url != '' && inputs.existing_webapp_url != null)) && (inputs.trigger_type != 'workflow_dispatch' || (inputs.run_e2e_tests != 'None' && inputs.run_e2e_tests != '' && inputs.run_e2e_tests != null)) + if: "!cancelled() && ((needs.deploy.result == 'success' && needs.deploy.outputs.WEB_APPURL != '') || (inputs.existing_webapp_url != '' && inputs.existing_webapp_url != null)) && (inputs.trigger_type != 'workflow_dispatch' || (inputs.run_e2e_tests != 'None' && inputs.run_e2e_tests != '' && inputs.run_e2e_tests != null))" needs: [deploy] uses: ./.github/workflows/test-automation-v2.yml with: @@ -77,7 +77,7 @@ jobs: secrets: inherit send-notification: - if: always() + if: "!cancelled()" needs: [deploy, e2e-test] uses: ./.github/workflows/job-send-notification.yml with: @@ -96,7 +96,7 @@ jobs: secrets: inherit cleanup-deployment: - if: always() && needs.deploy.result == 'success' && needs.deploy.outputs.RESOURCE_GROUP_NAME != '' && inputs.existing_webapp_url == '' && (inputs.trigger_type != 'workflow_dispatch' || inputs.cleanup_resources) + if: "!cancelled() && needs.deploy.result == 'success' && needs.deploy.outputs.RESOURCE_GROUP_NAME != '' && inputs.existing_webapp_url == '' && (inputs.trigger_type != 'workflow_dispatch' || inputs.cleanup_resources)" needs: [deploy, e2e-test] uses: ./.github/workflows/job-cleanup-deployment.yml with: diff --git a/.github/workflows/job-cleanup-deployment.yml b/.github/workflows/job-cleanup-deployment.yml index d1e7d311..b8a911c9 100644 --- a/.github/workflows/job-cleanup-deployment.yml +++ b/.github/workflows/job-cleanup-deployment.yml @@ -59,7 +59,7 @@ jobs: - name: Login to Azure shell: bash run: | - az login --service-principal -u ${{ secrets.AZURE_CLIENT_ID }} -p ${{ secrets.AZURE_CLIENT_SECRET }} --tenant ${{ secrets.AZURE_TENANT_ID }} + az login --service-principal --username ${{ secrets.AZURE_CLIENT_ID }} --password ${{ secrets.AZURE_CLIENT_SECRET }} --tenant ${{ secrets.AZURE_TENANT_ID }} az account set --subscription ${{ secrets.AZURE_SUBSCRIPTION_ID }} - name: Delete Resource Group (Optimized Cleanup) diff --git a/.github/workflows/job-deploy-windows.yml b/.github/workflows/job-deploy-linux.yml similarity index 91% rename from .github/workflows/job-deploy-windows.yml rename to .github/workflows/job-deploy-linux.yml index 646ca426..b5126253 100644 --- a/.github/workflows/job-deploy-windows.yml +++ b/.github/workflows/job-deploy-linux.yml @@ -31,10 +31,10 @@ on: outputs: WEB_APPURL: description: "Container Web App URL" - value: ${{ jobs.deploy-windows.outputs.WEB_APPURL }} + value: ${{ jobs.deploy-linux.outputs.WEB_APPURL }} jobs: - deploy-windows: + deploy-linux: runs-on: ubuntu-latest env: AZURE_DEV_COLLECT_TELEMETRY: ${{ vars.AZURE_DEV_COLLECT_TELEMETRY }} @@ -70,10 +70,10 @@ jobs: shell: bash run: | # If helm is already available on the runner, print version and skip installation - if command -v helm >/dev/null 2>&1; then - echo "helm already installed: $(helm version --short 2>/dev/null || true)" - exit 0 - fi + if command -v helm >/dev/null 2>&1; then + echo "helm already installed: $(helm version --short 2>/dev/null || true)" + exit 0 + fi # Ensure prerequisites are present sudo apt-get update @@ -106,7 +106,7 @@ jobs: - name: Login to Azure run: | - az login --service-principal -u ${{ secrets.AZURE_CLIENT_ID }} -p ${{ secrets.AZURE_CLIENT_SECRET }} --tenant ${{ secrets.AZURE_TENANT_ID }} + az login --service-principal --username ${{ secrets.AZURE_CLIENT_ID }} --password ${{ secrets.AZURE_CLIENT_SECRET }} --tenant ${{ secrets.AZURE_TENANT_ID }} az account set --subscription ${{ secrets.AZURE_SUBSCRIPTION_ID }} azd auth login --client-id ${{ secrets.AZURE_CLIENT_ID }} --client-secret ${{ secrets.AZURE_CLIENT_SECRET }} --tenant-id ${{ secrets.AZURE_TENANT_ID }} @@ -124,7 +124,7 @@ jobs: azd env set AZURE_ENV_OPENAI_LOCATION="${{ inputs.AZURE_ENV_OPENAI_LOCATION }}" azd env set AZURE_LOCATION="${{ inputs.AZURE_LOCATION }}" azd env set AZURE_RESOURCE_GROUP="${{ inputs.RESOURCE_GROUP_NAME }}" - azd env set AZURE_ENV_IMAGETAG="${{ inputs.IMAGE_TAG }}" + azd env set AZURE_ENV_IMAGE_TAG="${{ inputs.IMAGE_TAG }}" # Set AI model capacity parameters azd env set AZURE_ENV_MODEL_CAPACITY="150" @@ -167,7 +167,7 @@ jobs: # Get AKS node resource group if AKS exists if [ -n "$AZURE_AKS_NAME" ]; then - krg_name=$(az aks show --name "$AZURE_AKS_NAME" --resource-group "$RESOURCE_GROUP_NAME" --query "nodeResourceGroup" -o tsv || echo "") + krg_name=$(az aks show --name "$AZURE_AKS_NAME" --resource-group "${{ inputs.RESOURCE_GROUP_NAME }}" --query "nodeResourceGroup" -o tsv || echo "") if [ -n "$krg_name" ]; then echo "krg_name=$krg_name" >> $GITHUB_ENV echo "AKS node resource group: $krg_name" @@ -216,14 +216,6 @@ jobs: id: get_webapp_url shell: bash run: | - if az account show &> /dev/null; then - echo "Azure CLI is authenticated." - else - echo "Azure CLI is not authenticated. Logging in..." - az login --service-principal --username ${{ secrets.AZURE_CLIENT_ID }} --password ${{ secrets.AZURE_CLIENT_SECRET }} --tenant ${{ secrets.AZURE_TENANT_ID }} - fi - az account set --subscription ${{ secrets.AZURE_SUBSCRIPTION_ID }} - # Get the Web App URL and save it to GITHUB_OUTPUT echo "Retrieving Web App URL..." public_ip_name=$(az network public-ip list --resource-group ${{ env.krg_name }} --query "[?contains(name, 'kubernetes-')].name" -o tsv) @@ -300,7 +292,7 @@ jobs: if: always() shell: bash run: | - echo "## 🚀 Deploy Job Summary (Windows)" >> $GITHUB_STEP_SUMMARY + echo "## 🚀 Deploy Job Summary (Linux)" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY @@ -308,7 +300,7 @@ jobs: echo "| **Resource Group** | \`${{ inputs.RESOURCE_GROUP_NAME }}\` |" >> $GITHUB_STEP_SUMMARY echo "| **Configuration Type** | \`${{ inputs.WAF_ENABLED == 'true' && inputs.EXP == 'true' && 'WAF + EXP' || inputs.WAF_ENABLED == 'true' && inputs.EXP != 'true' && 'WAF + Non-EXP' || inputs.WAF_ENABLED != 'true' && inputs.EXP == 'true' && 'Non-WAF + EXP' || 'Non-WAF + Non-EXP' }}\` |" >> $GITHUB_STEP_SUMMARY echo "| **Azure Region (Infrastructure)** | \`${{ inputs.AZURE_LOCATION }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| **Azure OpenAI Region** | \`${{ inputs.AZURE_LOCATION }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Azure OpenAI Region** | \`${{ inputs.AZURE_ENV_OPENAI_LOCATION }}\` |" >> $GITHUB_STEP_SUMMARY echo "| **Docker Image Tag** | \`${{ inputs.IMAGE_TAG }}\` |" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY if [[ "${{ job.status }}" == "success" ]]; then diff --git a/.github/workflows/job-deploy.yml b/.github/workflows/job-deploy.yml index de052305..3415210f 100644 --- a/.github/workflows/job-deploy.yml +++ b/.github/workflows/job-deploy.yml @@ -118,7 +118,7 @@ jobs: - name: Login to Azure shell: bash run: | - az login --service-principal -u ${{ secrets.AZURE_CLIENT_ID }} -p ${{ secrets.AZURE_CLIENT_SECRET }} --tenant ${{ secrets.AZURE_TENANT_ID }} + az login --service-principal --username ${{ secrets.AZURE_CLIENT_ID }} --password ${{ secrets.AZURE_CLIENT_SECRET }} --tenant ${{ secrets.AZURE_TENANT_ID }} az account set --subscription ${{ secrets.AZURE_SUBSCRIPTION_ID }} - name: Run Quota Check @@ -130,21 +130,20 @@ jobs: # Path to the PowerShell script for quota check $quotaCheckScript = "Deployment/checkquota.ps1" - # Check if the script exists and is executable (not needed for PowerShell like chmod) + # Check if the script exists if (-not (Test-Path $quotaCheckScript)) { Write-Host "❌ Error: Quota check script not found." exit 1 } - # Run the script - .\Deployment\checkquota.ps1 + # Run the script and capture its output (stdout and stderr) + $output = & $quotaCheckScript 2>&1 + $exitCode = $LASTEXITCODE - # If the script fails, check for the failure message + # Check the execution output for the quota failure message $quotaFailedMessage = "No region with sufficient quota found" - $output = Get-Content "Deployment/checkquota.ps1" - - if ($output -contains $quotaFailedMessage) { - echo "QUOTA_FAILED=true" >> $GITHUB_ENV + if ($output -match [Regex]::Escape($quotaFailedMessage) -or $exitCode -ne 0) { + echo "QUOTA_FAILED=true" >> $env:GITHUB_ENV } env: AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} @@ -311,8 +310,8 @@ jobs: deploy-linux: name: Deploy needs: azure-setup - if: "!Cancelled() && needs.azure-setup.result == 'success'" - uses: ./.github/workflows/job-deploy-windows.yml + if: "!cancelled() && needs.azure-setup.result == 'success'" + uses: ./.github/workflows/job-deploy-linux.yml with: ENV_NAME: ${{ needs.azure-setup.outputs.ENV_NAME }} AZURE_ENV_OPENAI_LOCATION: ${{ needs.azure-setup.outputs.AZURE_ENV_OPENAI_LOCATION }} diff --git a/.github/workflows/test-automation-v2.yml b/.github/workflows/test-automation-v2.yml index 23e9d611..5bc9f857 100644 --- a/.github/workflows/test-automation-v2.yml +++ b/.github/workflows/test-automation-v2.yml @@ -22,7 +22,7 @@ on: env: url: ${{ inputs.Test_URL }} - accelerator_name: "Dkm" + accelerator_name: "DKM" test_suite: ${{ inputs.TEST_SUITE }} jobs: @@ -41,8 +41,12 @@ jobs: python-version: '3.13' - name: Login to Azure + env: + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} run: | - az login --service-principal -u ${{ secrets.AZURE_CLIENT_ID }} -p ${{ secrets.AZURE_CLIENT_SECRET }} --tenant ${{ secrets.AZURE_TENANT_ID }} + az login --service-principal --username "$AZURE_CLIENT_ID" --password "$AZURE_CLIENT_SECRET" --tenant "$AZURE_TENANT_ID" az account set --subscription ${{ secrets.AZURE_SUBSCRIPTION_ID }} - name: Install dependencies From d0cda0af822c11fb920c5bf053c3ac1fdde276d2 Mon Sep 17 00:00:00 2001 From: Rafi-Microsoft Date: Mon, 22 Dec 2025 14:23:25 +0530 Subject: [PATCH 33/49] copilot fixes --- .github/workflows/deploy-orchestrator.yml | 2 +- .github/workflows/job-deploy-linux.yml | 23 +++++----- .github/workflows/job-deploy.yml | 49 +++++++++++---------- .github/workflows/job-send-notification.yml | 2 +- .github/workflows/test-automation-v2.yml | 4 +- 5 files changed, 41 insertions(+), 39 deletions(-) diff --git a/.github/workflows/deploy-orchestrator.yml b/.github/workflows/deploy-orchestrator.yml index 6f0de5d4..1582dc9d 100644 --- a/.github/workflows/deploy-orchestrator.yml +++ b/.github/workflows/deploy-orchestrator.yml @@ -72,7 +72,7 @@ jobs: needs: [deploy] uses: ./.github/workflows/test-automation-v2.yml with: - Test_URL: ${{ needs.deploy.outputs.WEB_APPURL || inputs.existing_webapp_url }} + TEST_URL: ${{ needs.deploy.outputs.WEB_APPURL || inputs.existing_webapp_url }} TEST_SUITE: ${{ inputs.trigger_type == 'workflow_dispatch' && inputs.run_e2e_tests || 'GoldenPath-Testing' }} secrets: inherit diff --git a/.github/workflows/job-deploy-linux.yml b/.github/workflows/job-deploy-linux.yml index b5126253..321ac1d7 100644 --- a/.github/workflows/job-deploy-linux.yml +++ b/.github/workflows/job-deploy-linux.yml @@ -149,7 +149,7 @@ jobs: # Deploy azd up --no-prompt - echo "✅ azd deployment completed" + echo "✅ Azure Developer CLI (azd) deployment completed" - name: Get Deployment Outputs id: get_output @@ -167,10 +167,10 @@ jobs: # Get AKS node resource group if AKS exists if [ -n "$AZURE_AKS_NAME" ]; then - krg_name=$(az aks show --name "$AZURE_AKS_NAME" --resource-group "${{ inputs.RESOURCE_GROUP_NAME }}" --query "nodeResourceGroup" -o tsv || echo "") - if [ -n "$krg_name" ]; then - echo "krg_name=$krg_name" >> $GITHUB_ENV - echo "AKS node resource group: $krg_name" + aks_node_resource_group=$(az aks show --name "$AZURE_AKS_NAME" --resource-group "${{ inputs.RESOURCE_GROUP_NAME }}" --query "nodeResourceGroup" -o tsv || echo "") + if [ -n "$aks_node_resource_group" ]; then + echo "aks_node_resource_group=$aks_node_resource_group" >> $GITHUB_ENV + echo "AKS node resource group: $aks_node_resource_group" fi fi @@ -183,8 +183,9 @@ jobs: yes "@ $input | pwsh ./resourcedeployment.ps1 - Write-Host "Resource Group Name is ${{ env.RESOURCE_GROUP_NAME }}" - Write-Host "Kubernetes resource group is ${{ env.AZURE_AKS_NAME }}" + Write-Host "Resource Group: ${{ inputs.RESOURCE_GROUP_NAME }}" + Write-Host "AKS Cluster Name: ${{ env.AZURE_AKS_NAME }}" + Write-Host "AKS Node Resource Group: ${{ env.aks_node_resource_group }}" env: # From GitHub secrets (for login) AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} @@ -192,8 +193,8 @@ jobs: AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} - # From deployment outputs step (these come from $GITHUB_ENV) - RESOURCE_GROUP_NAME: ${{ env.RESOURCE_GROUP_NAME }} + # From workflow inputs and deployment outputs + RESOURCE_GROUP_NAME: ${{ inputs.RESOURCE_GROUP_NAME }} AZURE_RESOURCE_GROUP_ID: ${{ env.AZURE_RESOURCE_GROUP_ID }} STORAGE_ACCOUNT_NAME: ${{ env.STORAGE_ACCOUNT_NAME }} AZURE_SEARCH_SERVICE_NAME: ${{ env.AZURE_SEARCH_SERVICE_NAME }} @@ -218,8 +219,8 @@ jobs: run: | # Get the Web App URL and save it to GITHUB_OUTPUT echo "Retrieving Web App URL..." - public_ip_name=$(az network public-ip list --resource-group ${{ env.krg_name }} --query "[?contains(name, 'kubernetes-')].name" -o tsv) - fqdn=$(az network public-ip show --resource-group ${{ env.krg_name }} --name $public_ip_name --query "dnsSettings.fqdn" -o tsv) + public_ip_name=$(az network public-ip list --resource-group ${{ env.aks_node_resource_group }} --query "[?contains(name, 'kubernetes-')].name" -o tsv) + fqdn=$(az network public-ip show --resource-group ${{ env.aks_node_resource_group }} --name $public_ip_name --query "dnsSettings.fqdn" -o tsv) if [ -n "$fqdn" ]; then echo "WEB_APPURL=https://$fqdn" >> $GITHUB_OUTPUT echo "Web App URL is https://$fqdn" diff --git a/.github/workflows/job-deploy.yml b/.github/workflows/job-deploy.yml index 3415210f..561e4bd6 100644 --- a/.github/workflows/job-deploy.yml +++ b/.github/workflows/job-deploy.yml @@ -94,22 +94,23 @@ jobs: QUOTA_FAILED: ${{ steps.quota_failure_output.outputs.QUOTA_FAILED }} steps: - - name: Validate and Auto-Configure EXP + - name: Validate EXP Configuration shell: bash run: | echo "🔍 Validating EXP configuration..." - if [[ "${{ inputs.EXP }}" != "true" ]]; then - if [[ -n "${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}" ]]; then - echo "🔧 AUTO-ENABLING EXP: EXP parameter values were provided but EXP was not explicitly enabled." - echo "" - echo "You provided values for:" - [[ -n "${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}" ]] && echo " - Azure Log Analytics Workspace ID: '${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}'" - echo "" - echo "✅ Automatically enabling EXP to use these values." - echo "EXP=true" >> $GITHUB_ENV - echo "📌 EXP has been automatically enabled for this deployment." - fi + if [[ "${{ inputs.EXP }}" == "false" && -n "${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}" ]]; then + echo "⚠️ WARNING: EXP is disabled but Log Analytics Workspace ID was provided." + echo "The provided workspace ID will be ignored unless EXP is enabled." + echo "" + echo "Provided but unused:" + echo " - Azure Log Analytics Workspace ID: '${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}'" + echo "" + echo "To use these values, set EXP=true in your workflow dispatch." + elif [[ "${{ inputs.EXP }}" == "true" ]]; then + echo "✅ EXP is enabled" + else + echo "ℹ️ EXP is disabled" fi - name: Checkout Code @@ -247,18 +248,18 @@ jobs: if [[ "$BRANCH_NAME" == "main" ]]; then IMAGE_TAG="latest_waf" echo "Using main branch - image tag: latest_waf" - elif [[ "$BRANCH_NAME" == "dev" ]]; then - IMAGE_TAG="dev" - echo "Using dev branch - image tag: dev" - elif [[ "$BRANCH_NAME" == "demo" ]]; then - IMAGE_TAG="demo" - echo "Using demo branch - image tag: demo" - else - IMAGE_TAG="latest_waf" - echo "Using default for branch '$BRANCH_NAME' - image tag: latest_waf" - fi - - echo "Using existing Docker image tag: $IMAGE_TAG" + elif [[ "$BRANCH_NAME" == "dev" ]]; then + IMAGE_TAG="dev" + echo "Using dev branch - image tag: dev" + elif [[ "$BRANCH_NAME" == "demo" ]]; then + IMAGE_TAG="demo" + echo "Using demo branch - image tag: demo" + else + IMAGE_TAG="latest_waf" + echo "Using default for branch '$BRANCH_NAME' - image tag: latest_waf" + fi + + echo "Using existing Docker image tag: $IMAGE_TAG" echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_OUTPUT diff --git a/.github/workflows/job-send-notification.yml b/.github/workflows/job-send-notification.yml index 89189fa4..5ca35243 100644 --- a/.github/workflows/job-send-notification.yml +++ b/.github/workflows/job-send-notification.yml @@ -182,7 +182,7 @@ jobs: -d "$EMAIL_BODY" || echo "Failed to send test failure notification" - name: Send Existing URL Success Notification - if: inputs.deploy_result == 'skipped' && inputs.existing_webapp_url != '' && inputs.e2e_test_result == 'success' && (inputs.TEST_SUCCESS == 'true' || inputs.TEST_SUCCESS == '') + if: inputs.deploy_result == 'skipped' && inputs.existing_webapp_url != '' && inputs.e2e_test_result == 'success' shell: bash run: | RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" diff --git a/.github/workflows/test-automation-v2.yml b/.github/workflows/test-automation-v2.yml index 5bc9f857..8f48ed19 100644 --- a/.github/workflows/test-automation-v2.yml +++ b/.github/workflows/test-automation-v2.yml @@ -3,7 +3,7 @@ name: Test Automation Dkm-v2 on: workflow_call: inputs: - Test_URL: + TEST_URL: required: true type: string description: "Web URL for Dkm" @@ -21,7 +21,7 @@ on: value: ${{ jobs.test.outputs.TEST_REPORT_URL }} env: - url: ${{ inputs.Test_URL }} + url: ${{ inputs.TEST_URL }} accelerator_name: "DKM" test_suite: ${{ inputs.TEST_SUITE }} From 52bfbca4770567693149749c4c99e3cc1f512a82 Mon Sep 17 00:00:00 2001 From: Rafi-Microsoft Date: Mon, 22 Dec 2025 14:31:39 +0530 Subject: [PATCH 34/49] renamed a param back to original --- .github/workflows/job-deploy-linux.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/job-deploy-linux.yml b/.github/workflows/job-deploy-linux.yml index 321ac1d7..af54b815 100644 --- a/.github/workflows/job-deploy-linux.yml +++ b/.github/workflows/job-deploy-linux.yml @@ -167,10 +167,10 @@ jobs: # Get AKS node resource group if AKS exists if [ -n "$AZURE_AKS_NAME" ]; then - aks_node_resource_group=$(az aks show --name "$AZURE_AKS_NAME" --resource-group "${{ inputs.RESOURCE_GROUP_NAME }}" --query "nodeResourceGroup" -o tsv || echo "") - if [ -n "$aks_node_resource_group" ]; then - echo "aks_node_resource_group=$aks_node_resource_group" >> $GITHUB_ENV - echo "AKS node resource group: $aks_node_resource_group" + krg_name=$(az aks show --name "$AZURE_AKS_NAME" --resource-group "${{ inputs.RESOURCE_GROUP_NAME }}" --query "nodeResourceGroup" -o tsv || echo "") + if [ -n "$krg_name" ]; then + echo "krg_name=$krg_name" >> $GITHUB_ENV + echo "AKS node resource group: $krg_name" fi fi @@ -185,7 +185,7 @@ jobs: $input | pwsh ./resourcedeployment.ps1 Write-Host "Resource Group: ${{ inputs.RESOURCE_GROUP_NAME }}" Write-Host "AKS Cluster Name: ${{ env.AZURE_AKS_NAME }}" - Write-Host "AKS Node Resource Group: ${{ env.aks_node_resource_group }}" + Write-Host "AKS Node Resource Group: ${{ env.krg_name }}" env: # From GitHub secrets (for login) AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} @@ -219,8 +219,8 @@ jobs: run: | # Get the Web App URL and save it to GITHUB_OUTPUT echo "Retrieving Web App URL..." - public_ip_name=$(az network public-ip list --resource-group ${{ env.aks_node_resource_group }} --query "[?contains(name, 'kubernetes-')].name" -o tsv) - fqdn=$(az network public-ip show --resource-group ${{ env.aks_node_resource_group }} --name $public_ip_name --query "dnsSettings.fqdn" -o tsv) + public_ip_name=$(az network public-ip list --resource-group ${{ env.krg_name }} --query "[?contains(name, 'kubernetes-')].name" -o tsv) + fqdn=$(az network public-ip show --resource-group ${{ env.krg_name }} --name $public_ip_name --query "dnsSettings.fqdn" -o tsv) if [ -n "$fqdn" ]; then echo "WEB_APPURL=https://$fqdn" >> $GITHUB_OUTPUT echo "Web App URL is https://$fqdn" From 2c1dd07c85135cbc3280fc9cbbe736743363a374 Mon Sep 17 00:00:00 2001 From: Vamshi-Microsoft Date: Fri, 9 Jan 2026 14:29:36 +0530 Subject: [PATCH 35/49] Added permissions section and removed Curl Azure CLI setup steps and replace with Azure setup actions --- .github/workflows/CI.yml | 22 +++++----------------- .github/workflows/test-automation.yml | 4 +++- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index f9114cff..0161b923 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -15,7 +15,9 @@ on: - 'tests/**' schedule: - cron: "0 10,22 * * *" # Runs at 10:00 AM and 10:00 PM GMT - +permissions: + contents: read + actions: read env: GPT_CAPACITY: 150 TEXT_EMBEDDING_CAPACITY: 200 @@ -35,12 +37,6 @@ jobs: - name: Checkout Code uses: actions/checkout@v5 # Checks out your repository - - name: Install Azure CLI - shell: bash - run: | - curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash - az --version # Verify installation - - name: Install Kubernetes CLI (kubectl) shell: bash run: | @@ -144,10 +140,8 @@ jobs: - name: Install Bicep CLI run: az bicep install - - name: Install Azure Developer CLI - run: | - curl -fsSL https://aka.ms/install-azd.sh | bash - shell: bash + - name: Install azd + uses: Azure/setup-azd@v2 - name: Set Deployment Region run: | @@ -406,12 +400,6 @@ jobs: VALID_REGION: ${{ needs.deploy.outputs.VALID_REGION }} steps: - - name: Install Azure CLI - shell: bash - run: | - curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash - az --version # Verify installation - - name: Login to Azure shell: bash run: | diff --git a/.github/workflows/test-automation.yml b/.github/workflows/test-automation.yml index 6bf45965..dd6e5a57 100644 --- a/.github/workflows/test-automation.yml +++ b/.github/workflows/test-automation.yml @@ -15,7 +15,9 @@ on: env: url: ${{ inputs.DKM_URL }} accelerator_name: "DKM" - +permissions: + contents: read + actions: read jobs: test: runs-on: ubuntu-latest From edf6831ba6d4726b1d763353e56631f1e72957ba Mon Sep 17 00:00:00 2001 From: Vamshi-Microsoft Date: Wed, 14 Jan 2026 17:34:42 +0530 Subject: [PATCH 36/49] fix: Post deployment to continue on error --- .github/workflows/CI.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 0161b923..3d72c38f 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -350,6 +350,7 @@ jobs: - name: Run Post Deployment Script shell: pwsh + continue-on-error: true run: | Write-Host "Running post deployment script to upload files..." cd Deployment From 2e9759e4ce78f5022bd4ebb4eebdd86531355ce8 Mon Sep 17 00:00:00 2001 From: Rafi-Microsoft Date: Wed, 28 Jan 2026 12:27:24 +0530 Subject: [PATCH 37/49] copilot suggested changes --- .github/workflows/deploy-linux.yml | 8 ++++---- .github/workflows/deploy-orchestrator.yml | 2 +- .github/workflows/job-cleanup-deployment.yml | 1 - .github/workflows/job-deploy-linux.yml | 4 ++-- .github/workflows/job-deploy.yml | 16 ++++------------ .github/workflows/test-automation-v2.yml | 8 ++++++++ 6 files changed, 19 insertions(+), 20 deletions(-) diff --git a/.github/workflows/deploy-linux.yml b/.github/workflows/deploy-linux.yml index 7fd7462f..1ef85eda 100644 --- a/.github/workflows/deploy-linux.yml +++ b/.github/workflows/deploy-linux.yml @@ -6,7 +6,7 @@ on: - dev - demo schedule: - - cron: "0 10,22 * * *" # Runs at 10:00 AM and 10:00 PM GMT + - cron: "0 10,22 * * *" # Runs at 10:00 AM and 10:00 PM UTC workflow_dispatch: inputs: @@ -74,9 +74,9 @@ jobs: with: azure_location: ${{ github.event.inputs.azure_location || 'australiaeast' }} resource_group_name: ${{ github.event.inputs.resource_group_name || '' }} - waf_enabled: ${{ github.event.inputs.waf_enabled == 'true' }} - EXP: ${{ github.event.inputs.EXP == 'true' }} - cleanup_resources: ${{ github.event.inputs.cleanup_resources == 'true' }} + waf_enabled: ${{ github.event.inputs.waf_enabled }} + EXP: ${{ github.event.inputs.EXP }} + cleanup_resources: ${{ github.event.inputs.cleanup_resources }} run_e2e_tests: ${{ github.event.inputs.run_e2e_tests || 'GoldenPath-Testing' }} AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ github.event.inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID || '' }} existing_webapp_url: ${{ github.event.inputs.existing_webapp_url || '' }} diff --git a/.github/workflows/deploy-orchestrator.yml b/.github/workflows/deploy-orchestrator.yml index 1582dc9d..b42ca2b8 100644 --- a/.github/workflows/deploy-orchestrator.yml +++ b/.github/workflows/deploy-orchestrator.yml @@ -68,7 +68,7 @@ jobs: secrets: inherit e2e-test: - if: "!cancelled() && ((needs.deploy.result == 'success' && needs.deploy.outputs.WEB_APPURL != '') || (inputs.existing_webapp_url != '' && inputs.existing_webapp_url != null)) && (inputs.trigger_type != 'workflow_dispatch' || (inputs.run_e2e_tests != 'None' && inputs.run_e2e_tests != '' && inputs.run_e2e_tests != null))" + if: "!cancelled() && ((needs.deploy.outputs.WEB_APPURL != '' && needs.deploy.outputs.WEB_APPURL != null) || (inputs.existing_webapp_url != '' && inputs.existing_webapp_url != null)) && (inputs.trigger_type != 'workflow_dispatch' || (inputs.run_e2e_tests != 'None' && inputs.run_e2e_tests != '' && inputs.run_e2e_tests != null))" needs: [deploy] uses: ./.github/workflows/test-automation-v2.yml with: diff --git a/.github/workflows/job-cleanup-deployment.yml b/.github/workflows/job-cleanup-deployment.yml index b8a911c9..30e518e3 100644 --- a/.github/workflows/job-cleanup-deployment.yml +++ b/.github/workflows/job-cleanup-deployment.yml @@ -82,7 +82,6 @@ jobs: if: always() shell: bash run: | - azd auth logout || true az logout || echo "Warning: Failed to logout from Azure CLI" echo "Logged out from Azure." diff --git a/.github/workflows/job-deploy-linux.yml b/.github/workflows/job-deploy-linux.yml index af54b815..b8066ec5 100644 --- a/.github/workflows/job-deploy-linux.yml +++ b/.github/workflows/job-deploy-linux.yml @@ -76,8 +76,8 @@ jobs: fi # Ensure prerequisites are present - sudo apt-get update - sudo apt-get install -y apt-transport-https ca-certificates curl gnupg lsb-release + sudo apt-get update + sudo apt-get install -y apt-transport-https ca-certificates curl gnupg lsb-release # Ensure keyrings dir exists sudo mkdir -p /usr/share/keyrings diff --git a/.github/workflows/job-deploy.yml b/.github/workflows/job-deploy.yml index 561e4bd6..40be0127 100644 --- a/.github/workflows/job-deploy.yml +++ b/.github/workflows/job-deploy.yml @@ -172,6 +172,10 @@ jobs: id: set_region shell: bash run: | + if [[ -z "$VALID_REGION" ]]; then + echo "❌ ERROR: VALID_REGION is not set. The quota check script (Deployment/checkquota.ps1) must set this variable before this step runs." >&2 + exit 1 + fi echo "Selected Region from Quota Check: $VALID_REGION" echo "AZURE_ENV_OPENAI_LOCATION=$VALID_REGION" >> $GITHUB_ENV echo "AZURE_ENV_OPENAI_LOCATION=$VALID_REGION" >> $GITHUB_OUTPUT @@ -225,18 +229,6 @@ jobs: echo "RESOURCE_GROUP_NAME=$RESOURCE_GROUP_NAME" >> $GITHUB_OUTPUT echo "RESOURCE_GROUP_NAME=$RESOURCE_GROUP_NAME" >> $GITHUB_ENV - - name: Generate Unique Solution Prefix - id: generate_solution_prefix - shell: bash - run: | - set -e - COMMON_PART="psldg" - TIMESTAMP=$(date +%s) - UPDATED_TIMESTAMP=$(echo $TIMESTAMP | tail -c 6) - UNIQUE_SOLUTION_PREFIX="${COMMON_PART}${UPDATED_TIMESTAMP}" - echo "SOLUTION_PREFIX=${UNIQUE_SOLUTION_PREFIX}" >> $GITHUB_ENV - echo "Generated SOLUTION_PREFIX: ${UNIQUE_SOLUTION_PREFIX}" - - name: Determine Docker Image Tag id: determine_image_tag run: | diff --git a/.github/workflows/test-automation-v2.yml b/.github/workflows/test-automation-v2.yml index 8f48ed19..a3a87299 100644 --- a/.github/workflows/test-automation-v2.yml +++ b/.github/workflows/test-automation-v2.yml @@ -63,6 +63,14 @@ jobs: echo "ERROR: No URL provided for testing" exit 1 fi + + # Validate test suite is implemented + if [ "${{ env.test_suite }}" == "Smoke-Testing" ]; then + echo "ERROR: Smoke-Testing is not yet implemented" + echo "Available test suites: GoldenPath-Testing" + exit 1 + fi + echo "Testing URL: ${{ env.url }}" echo "Test Suite: ${{ env.test_suite }}" From 61739e9d7712a8247b44ea287405bbc68093258e Mon Sep 17 00:00:00 2001 From: Rafi-Microsoft Date: Wed, 28 Jan 2026 13:23:33 +0530 Subject: [PATCH 38/49] boolean values fix --- .github/workflows/deploy-linux.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-linux.yml b/.github/workflows/deploy-linux.yml index 1ef85eda..93b5ae00 100644 --- a/.github/workflows/deploy-linux.yml +++ b/.github/workflows/deploy-linux.yml @@ -74,9 +74,9 @@ jobs: with: azure_location: ${{ github.event.inputs.azure_location || 'australiaeast' }} resource_group_name: ${{ github.event.inputs.resource_group_name || '' }} - waf_enabled: ${{ github.event.inputs.waf_enabled }} - EXP: ${{ github.event.inputs.EXP }} - cleanup_resources: ${{ github.event.inputs.cleanup_resources }} + waf_enabled: ${{ github.event.inputs.waf_enabled || false }} + EXP: ${{ github.event.inputs.EXP || false }} + cleanup_resources: ${{ github.event.inputs.cleanup_resources || false }} run_e2e_tests: ${{ github.event.inputs.run_e2e_tests || 'GoldenPath-Testing' }} AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ github.event.inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID || '' }} existing_webapp_url: ${{ github.event.inputs.existing_webapp_url || '' }} From 26f1c1671886d158559a6b260fa2490804c4958f Mon Sep 17 00:00:00 2001 From: Rafi-Microsoft Date: Thu, 29 Jan 2026 10:12:04 +0530 Subject: [PATCH 39/49] fixes v1 --- .github/workflows/deploy-linux.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-linux.yml b/.github/workflows/deploy-linux.yml index 93b5ae00..392fcc08 100644 --- a/.github/workflows/deploy-linux.yml +++ b/.github/workflows/deploy-linux.yml @@ -74,9 +74,9 @@ jobs: with: azure_location: ${{ github.event.inputs.azure_location || 'australiaeast' }} resource_group_name: ${{ github.event.inputs.resource_group_name || '' }} - waf_enabled: ${{ github.event.inputs.waf_enabled || false }} - EXP: ${{ github.event.inputs.EXP || false }} - cleanup_resources: ${{ github.event.inputs.cleanup_resources || false }} + waf_enabled: ${{ github.event.inputs.waf_enabled == true }} + EXP: ${{ github.event.inputs.EXP == true }} + cleanup_resources: ${{ github.event.inputs.cleanup_resources == true }} run_e2e_tests: ${{ github.event.inputs.run_e2e_tests || 'GoldenPath-Testing' }} AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ github.event.inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID || '' }} existing_webapp_url: ${{ github.event.inputs.existing_webapp_url || '' }} From 8033a84a4db8811c3d6ee9d3b3a4aa1112a47f1b Mon Sep 17 00:00:00 2001 From: Vamshi-Microsoft Date: Fri, 30 Jan 2026 16:08:33 +0530 Subject: [PATCH 40/49] Integrated GP and Smoke Testing --- .github/workflows/test-automation-v2.yml | 13 +++--- tests/e2e-test/pytest.ini | 3 ++ tests/e2e-test/tests/test_dkm_functional.py | 48 ++++++++++----------- 3 files changed, 33 insertions(+), 31 deletions(-) diff --git a/.github/workflows/test-automation-v2.yml b/.github/workflows/test-automation-v2.yml index a3a87299..fe64a1ec 100644 --- a/.github/workflows/test-automation-v2.yml +++ b/.github/workflows/test-automation-v2.yml @@ -64,13 +64,6 @@ jobs: exit 1 fi - # Validate test suite is implemented - if [ "${{ env.test_suite }}" == "Smoke-Testing" ]; then - echo "ERROR: Smoke-Testing is not yet implemented" - echo "Available test suites: GoldenPath-Testing" - exit 1 - fi - echo "Testing URL: ${{ env.url }}" echo "Test Suite: ${{ env.test_suite }}" @@ -103,6 +96,8 @@ jobs: id: test1 run: | if [ "${{ env.test_suite }}" == "GoldenPath-Testing" ]; then + xvfb-run pytest -m goldenpath --headed --html=report/report.html --self-contained-html + else xvfb-run pytest --headed --html=report/report.html --self-contained-html fi working-directory: tests/e2e-test @@ -118,6 +113,8 @@ jobs: if: ${{ steps.test1.outcome == 'failure' }} run: | if [ "${{ env.test_suite }}" == "GoldenPath-Testing" ]; then + xvfb-run pytest -m goldenpath --headed --html=report/report.html --self-contained-html + else xvfb-run pytest --headed --html=report/report.html --self-contained-html fi working-directory: tests/e2e-test @@ -133,6 +130,8 @@ jobs: if: ${{ steps.test2.outcome == 'failure' }} run: | if [ "${{ env.test_suite }}" == "GoldenPath-Testing" ]; then + xvfb-run pytest -m goldenpath --headed --html=report/report.html --self-contained-html + else xvfb-run pytest --headed --html=report/report.html --self-contained-html fi working-directory: tests/e2e-test diff --git a/tests/e2e-test/pytest.ini b/tests/e2e-test/pytest.ini index 76eb64fc..a18b0949 100644 --- a/tests/e2e-test/pytest.ini +++ b/tests/e2e-test/pytest.ini @@ -4,3 +4,6 @@ log_cli_level = INFO log_file = logs/tests.log log_file_level = INFO addopts = -p no:warnings + +markers = + goldenpath: Golden Path tests \ No newline at end of file diff --git a/tests/e2e-test/tests/test_dkm_functional.py b/tests/e2e-test/tests/test_dkm_functional.py index fd068d76..0bad5f00 100644 --- a/tests/e2e-test/tests/test_dkm_functional.py +++ b/tests/e2e-test/tests/test_dkm_functional.py @@ -42,7 +42,7 @@ def capture_screenshot(page, step_name, test_prefix="test"): pass -@pytest.mark.smoke +@pytest.mark.goldenpath def test_golden_path_dkm(login_logout, request): """ Test Case 10591: Golden Path-DKM-test golden path demo script works properly @@ -283,7 +283,7 @@ def test_golden_path_dkm(login_logout, request): logger.removeHandler(handler) -@pytest.mark.smoke + def test_upload_default_github_data(login_logout, request): """ Test Case 10661: DKM-Upload default GitHub repo sample data @@ -350,7 +350,7 @@ def test_upload_default_github_data(login_logout, request): logger.removeHandler(handler) -@pytest.mark.smoke + def test_search_functionality(login_logout, request): """ Test Case 10671: DKM-Verify the search functionality @@ -416,7 +416,7 @@ def test_search_functionality(login_logout, request): logger.removeHandler(handler) -@pytest.mark.smoke + def test_chat_selected_document(login_logout, request): """ Test Case 10704: DKM-Test chat selected document @@ -496,7 +496,7 @@ def test_chat_selected_document(login_logout, request): logger.removeHandler(handler) -@pytest.mark.smoke + def test_chat_multiple_selected_documents(login_logout, request): """ Test Case 10705: DKM-Test chat multiple selected documents @@ -575,7 +575,7 @@ def test_chat_multiple_selected_documents(login_logout, request): logger.removeHandler(handler) -@pytest.mark.smoke + def test_chat_all_documents(login_logout, request): """ Test Case 10706: DKM-Test chat all documents @@ -634,7 +634,7 @@ def test_chat_all_documents(login_logout, request): logger.removeHandler(handler) -@pytest.mark.smoke + def test_jailbreak_questions(login_logout, request): """ Test Case 10707: DKM-Test questions to jailbreak @@ -702,7 +702,7 @@ def test_jailbreak_questions(login_logout, request): logger.removeHandler(handler) -@pytest.mark.smoke + def test_web_knowledge_questions(login_logout, request): """ Test Case 10708: DKM-Test questions to ask web knowledge @@ -761,7 +761,7 @@ def test_web_knowledge_questions(login_logout, request): logger.removeHandler(handler) -@pytest.mark.smoke + def test_send_button_disabled_by_default(login_logout, request): """ Test Case 14111: Bug-13861-DKM - Send prompt icon should be disabled by default @@ -827,7 +827,7 @@ def test_send_button_disabled_by_default(login_logout, request): logger.removeHandler(handler) -@pytest.mark.smoke + def test_validate_empty_spaces_chat_input(login_logout, request): """ Test Case 26217: DKM - Validate chat input handling for Empty / only-spaces @@ -899,7 +899,7 @@ def test_validate_empty_spaces_chat_input(login_logout, request): logger.removeHandler(handler) -@pytest.mark.smoke + def test_upload_different_file_types(login_logout, request): """ Test Case 10664: DKM-Upload one file of each supported filetype @@ -964,7 +964,7 @@ def test_upload_different_file_types(login_logout, request): logger.removeHandler(handler) -@pytest.mark.smoke + def test_upload_large_file(login_logout, request): """ Test Case 10665: OOS_DKM-Upload very large file size @@ -1030,7 +1030,7 @@ def test_upload_large_file(login_logout, request): logger.removeHandler(handler) -@pytest.mark.smoke + def test_upload_zero_byte_file(login_logout, request): """ Test Case 10666: DKM-Upload zero byte file @@ -1085,7 +1085,7 @@ def test_upload_zero_byte_file(login_logout, request): logger.removeHandler(handler) -@pytest.mark.smoke + def test_upload_unsupported_file(login_logout, request): """ Test Case 10667: DKM-Upload unsupported file @@ -1140,7 +1140,7 @@ def test_upload_unsupported_file(login_logout, request): logger.removeHandler(handler) -@pytest.mark.smoke + def test_documents_scrolling_pagination(login_logout, request): """ Test Case 10670: DKM-test documents section scrolling and pagination @@ -1201,7 +1201,7 @@ def test_documents_scrolling_pagination(login_logout, request): logger.removeHandler(handler) -@pytest.mark.smoke + def test_search_with_time_filter(login_logout, request): """ Test Case 10672: DKM-Test search documents with time filter @@ -1272,7 +1272,7 @@ def test_search_with_time_filter(login_logout, request): logger.removeHandler(handler) -@pytest.mark.smoke + def test_left_pane_filters(login_logout, request): """ Test Case 10700: DKM-Test left pane filters @@ -1341,7 +1341,7 @@ def test_left_pane_filters(login_logout, request): logger.removeHandler(handler) -@pytest.mark.smoke + def test_left_pane_and_search_filters(login_logout, request): """ Test Case 10702: DKM-Test left pane filters collision with search filters @@ -1412,7 +1412,7 @@ def test_left_pane_and_search_filters(login_logout, request): logger.removeHandler(handler) -@pytest.mark.smoke + def test_document_details_preview(login_logout, request): """ Test Case 10703: DKM-Test document details preview @@ -1492,7 +1492,7 @@ def test_document_details_preview(login_logout, request): logger.removeHandler(handler) -@pytest.mark.smoke + def test_confirm_references_citations(login_logout, request): """ Test Case 10710: DKM-Confirm references or citations in response @@ -1564,7 +1564,7 @@ def test_confirm_references_citations(login_logout, request): logger.removeHandler(handler) -@pytest.mark.smoke + def test_bug_sensitive_question_stuck(login_logout, request): """ Test Case 13539: Bug 12794 - Response Not Generated for Sensitive Question @@ -1630,7 +1630,7 @@ def test_bug_sensitive_question_stuck(login_logout, request): logger.removeHandler(handler) -@pytest.mark.smoke + def test_bug_chat_session_cleared(login_logout, request): """ Test Case 14704: Bug-13797-DKM-Chat session cleared when switch tabs @@ -1706,7 +1706,7 @@ def test_bug_chat_session_cleared(login_logout, request): logger.removeHandler(handler) -@pytest.mark.smoke + def test_bug_text_file_download(login_logout, request): """ Test Case 16787: Bug 16600 - Text file getting downloaded on click @@ -1768,7 +1768,7 @@ def test_bug_text_file_download(login_logout, request): logger.removeHandler(handler) -@pytest.mark.smoke + def test_bug_clear_all_button(login_logout, request): """ Test Case 16788: Bug 16599 - Clear All Button should reset search box From e79a5fdf480c8c355f1c95df8455de5643e8df36 Mon Sep 17 00:00:00 2001 From: Rafi-Microsoft Date: Fri, 30 Jan 2026 16:34:18 +0530 Subject: [PATCH 41/49] updated the post deployment script --- Deployment/send-filestoendpoint.psm1 | 101 ++++++++++++++++++++------- 1 file changed, 76 insertions(+), 25 deletions(-) diff --git a/Deployment/send-filestoendpoint.psm1 b/Deployment/send-filestoendpoint.psm1 index e7964467..05a2fe55 100644 --- a/Deployment/send-filestoendpoint.psm1 +++ b/Deployment/send-filestoendpoint.psm1 @@ -27,6 +27,10 @@ function Send-FilesToEndpoint { $totalFiles = $files.Count $currentFileIndex = 0 + $maxRetries = 3 + $retryDelaySeconds = 5 + $failedFiles = @() + $successfulFiles = 0 foreach ($file in $files) { $currentFileIndex++ @@ -35,43 +39,70 @@ function Send-FilesToEndpoint { # Check file size if ($file.Length -eq 0) { - Write-Host "File cannot be uploaded: $($file.Name) (File size is 0)" + Write-Host "⚠️ File cannot be uploaded: $($file.Name) (File size is 0)" -ForegroundColor Yellow + $failedFiles += @{FileName = $file.Name; Reason = "File size is 0"} continue } # Check file type $allowedExtensions = @(".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".pdf", ".tif", ".tiff", ".jpg", ".jpeg", ".png", ".bmp", ".txt") if (-Not ($allowedExtensions -contains $file.Extension.ToLower())) { - Write-Host "File cannot be uploaded: $($file.Name) (Unsupported file type)" + Write-Host "⚠️ File cannot be uploaded: $($file.Name) (Unsupported file type)" -ForegroundColor Yellow + $failedFiles += @{FileName = $file.Name; Reason = "Unsupported file type"} continue } - try { - # Read the file content as byte array - $fileContent = [System.IO.File]::ReadAllBytes($file.FullName) + # Retry logic for file upload + $uploadSuccess = $false + $attempt = 0 + + while ($attempt -lt $maxRetries -and -not $uploadSuccess) { + $attempt++ + try { + if ($attempt -gt 1) { + Write-Host "🔄 Retry attempt $attempt of $maxRetries for file: $($file.Name)" -ForegroundColor Cyan + Start-Sleep -Seconds $retryDelaySeconds + } + + # Read the file content as byte array + $fileContent = [System.IO.File]::ReadAllBytes($file.FullName) - # Create the multipart form data content - $content = [System.Net.Http.MultipartFormDataContent]::new() - $fileContentByteArray = [System.Net.Http.ByteArrayContent]::new($fileContent) - $fileContentByteArray.Headers.ContentDisposition = [System.Net.Http.Headers.ContentDispositionHeaderValue]::new("form-data") - $fileContentByteArray.Headers.ContentDisposition.Name = '"file"' - $fileContentByteArray.Headers.ContentDisposition.FileName = '"' + $file.Name + '"' - $content.Add($fileContentByteArray) + # Create the multipart form data content + $content = [System.Net.Http.MultipartFormDataContent]::new() + $fileContentByteArray = [System.Net.Http.ByteArrayContent]::new($fileContent) + $fileContentByteArray.Headers.ContentDisposition = [System.Net.Http.Headers.ContentDispositionHeaderValue]::new("form-data") + $fileContentByteArray.Headers.ContentDisposition.Name = '"file"' + $fileContentByteArray.Headers.ContentDisposition.FileName = '"' + $file.Name + '"' + $content.Add($fileContentByteArray) - # Upload the file content to the HTTP endpoint - $response = $httpClient.PostAsync($EndpointUrl, $content).GetAwaiter().GetResult() - - - # Check the response status - if ($response.IsSuccessStatusCode) { - Write-Host "File uploaded successfully: $($file.Name)" - } - else { - Write-Error "Failed to upload file: $($file.Name). Status code: $($response.StatusCode)" + # Upload the file content to the HTTP endpoint + $response = $httpClient.PostAsync($EndpointUrl, $content).GetAwaiter().GetResult() + + + # Check the response status + if ($response.IsSuccessStatusCode) { + Write-Host "✅ File uploaded successfully: $($file.Name)" -ForegroundColor Green + $uploadSuccess = $true + $successfulFiles++ + } + else { + $statusCode = $response.StatusCode + if ($attempt -lt $maxRetries) { + Write-Host "⚠️ Failed to upload file: $($file.Name). Status code: $statusCode. Will retry..." -ForegroundColor Yellow + } else { + Write-Host "❌ Failed to upload file: $($file.Name). Status code: $statusCode. Max retries reached." -ForegroundColor Red + $failedFiles += @{FileName = $file.Name; Reason = "HTTP Status: $statusCode"} + } + } + } + catch { + if ($attempt -lt $maxRetries) { + Write-Host "⚠️ Error uploading file: $($file.Name). Error: $($_.Exception.Message). Will retry..." -ForegroundColor Yellow + } else { + Write-Host "❌ Error uploading file: $($file.Name). Error: $($_.Exception.Message). Max retries reached." -ForegroundColor Red + $failedFiles += @{FileName = $file.Name; Reason = $_.Exception.Message} + } } - } - catch { - Write-Error "An error occurred while uploading the file: $($file.Name). Error: $_" } } # Dispose HttpClient @@ -79,6 +110,26 @@ function Send-FilesToEndpoint { # Clear the progress bar Write-Progress -Activity "Uploading Files" -Status "Completed" -PercentComplete 100 + + # Print summary report + Write-Host "`n========================================" -ForegroundColor Cyan + Write-Host "📊 File Upload Summary" -ForegroundColor Cyan + Write-Host "========================================" -ForegroundColor Cyan + Write-Host "Total files processed: $totalFiles" -ForegroundColor White + Write-Host "✅ Successfully uploaded: $successfulFiles" -ForegroundColor Green + Write-Host "❌ Failed uploads: $($failedFiles.Count)" -ForegroundColor Red + + if ($failedFiles.Count -gt 0) { + Write-Host "`n❌ Failed Files Details:" -ForegroundColor Red + foreach ($failed in $failedFiles) { + Write-Host " • $($failed.FileName) - Reason: $($failed.Reason)" -ForegroundColor Yellow + } + Write-Host "`n⚠️ Warning: Some files failed to upload after $maxRetries retry attempts." -ForegroundColor Yellow + Write-Host "You can manually retry uploading the failed files later." -ForegroundColor Yellow + } else { + Write-Host "`n✅ All files uploaded successfully!" -ForegroundColor Green + } + Write-Host "========================================`n" -ForegroundColor Cyan } Export-ModuleMember -Function Send-FilesToEndpoint \ No newline at end of file From 4c773f0d2697a947d4ab1755a3a94da9b847a932 Mon Sep 17 00:00:00 2001 From: Kanchan-Microsoft Date: Sat, 31 Jan 2026 13:41:26 +0530 Subject: [PATCH 42/49] fixed for waf and cleanup --- .github/workflows/deploy-linux.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-linux.yml b/.github/workflows/deploy-linux.yml index 392fcc08..64c8ce29 100644 --- a/.github/workflows/deploy-linux.yml +++ b/.github/workflows/deploy-linux.yml @@ -74,9 +74,9 @@ jobs: with: azure_location: ${{ github.event.inputs.azure_location || 'australiaeast' }} resource_group_name: ${{ github.event.inputs.resource_group_name || '' }} - waf_enabled: ${{ github.event.inputs.waf_enabled == true }} - EXP: ${{ github.event.inputs.EXP == true }} - cleanup_resources: ${{ github.event.inputs.cleanup_resources == true }} + waf_enabled: ${{ github.event.inputs.waf_enabled == 'true' }} + EXP: ${{ github.event.inputs.EXP == 'true' }} + cleanup_resources: ${{ github.event.inputs.cleanup_resources == 'true' }} run_e2e_tests: ${{ github.event.inputs.run_e2e_tests || 'GoldenPath-Testing' }} AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ github.event.inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID || '' }} existing_webapp_url: ${{ github.event.inputs.existing_webapp_url || '' }} From 7ac7bf1fdf82c5de021b8a77e8a55cfa692e75f5 Mon Sep 17 00:00:00 2001 From: "Niraj Chaudhari (Persistent Systems Inc)" Date: Mon, 9 Feb 2026 15:49:46 +0530 Subject: [PATCH 43/49] update troubleshoot document --- docs/TroubleShootingSteps.md | 738 +++++++-------------------------- docs/images/AzureHomePage.png | Bin 0 -> 89286 bytes docs/images/resourcegroup1.png | Bin 0 -> 71702 bytes 3 files changed, 146 insertions(+), 592 deletions(-) create mode 100644 docs/images/AzureHomePage.png create mode 100644 docs/images/resourcegroup1.png diff --git a/docs/TroubleShootingSteps.md b/docs/TroubleShootingSteps.md index effd48cd..0b177d7e 100644 --- a/docs/TroubleShootingSteps.md +++ b/docs/TroubleShootingSteps.md @@ -1,609 +1,163 @@ # 🛠️ Troubleshooting - + When deploying Azure resources, you may come across different error codes that stop or delay the deployment process. This section lists some of the most common errors along with possible causes and step-by-step resolutions. - + Use these as quick reference guides to unblock your deployments. -> **💡 Need deployment recovery help?** If your deployment failed and you need to start over, see the [Recover from Failed Deployment](./DeploymentGuide.md#recover-from-failed-deployment) section in the deployment guide. - -## Error Codes - -
-ReadOnlyDisabledSubscription - -- Check if you have an active subscription before starting the deployment. - -
-
- MissingSubscriptionRegistration/ AllowBringYourOwnPublicIpAddress/ InvalidAuthenticationToken - -Enable `AllowBringYourOwnPublicIpAddress` Feature - -Before deploying the resources, you may need to enable the **Bring Your Own Public IP Address** feature in Azure. This is required only once per subscription. - -### Steps - -1. **Run the following command to register the feature:** - - ```bash - az feature register --namespace Microsoft.Network --name AllowBringYourOwnPublicIpAddress - ``` - -2. **Wait for the registration to complete.** - You can check the status using: - - ```bash - az feature show --namespace Microsoft.Network --name AllowBringYourOwnPublicIpAddress --query properties.state - ``` - -3. **The output should show:** - "Registered" - -4. **Once the feature is registered, refresh the provider:** - - ```bash - az provider register --namespace Microsoft.Network - ``` - - 💡 Note: Feature registration may take several minutes to complete. This needs to be done only once per Azure subscription. - -
-
-ResourceGroupNotFound - -## Option 1 - -### Steps - -1. Go to [Azure Portal](https:/portal.azure.com/#home). - -2. Click on the **"Resource groups"** option available on the Azure portal home page. - ![alt text](./images/troubleshooting/rg_not_found0.png) - -3. In the Resource Groups search bar, search for the resource group you intend to target for deployment. If it exists, you can proceed with using it. - ![alt text](./images/troubleshooting/rg_not_found.png) - -## Option 2 - -- This error can occur if you deploy the template using the same .env file - from a previous deployment. -- To avoid this issue, create a new environment before redeploying. -- You can use the following command to create a new environment: - -``` -azd env new -``` - -
- -
-ResourceGroupBeingDeleted - -To prevent this issue, please ensure that the resource group you are targeting for deployment is not currently being deleted. You can follow steps to verify resource group is being deleted or not. - -### Steps: - -1. Go to [Azure Portal](https://portal.azure.com/#home) -2. Go to resource group option and search for targeted resource group -3. If Targeted resource group is there and deletion for this is in progress, it means you cannot use this, you can create new or use any other resource group - -
- -
-InternalSubscriptionIsOverQuotaForSku/ManagedEnvironmentProvisioningError - -Quotas are applied per resource group, subscriptions, accounts, and other scopes. For example, your subscription might be configured to limit the number of vCPUs for a region. If you attempt to deploy a virtual machine with more vCPUs than the permitted amount, you receive an error that the quota was exceeded. -For PowerShell, use the `Get-AzVMUsage` cmdlet to find virtual machine quotas. - -```ps -Get-AzVMUsage -Location "West US" -``` - -based on available quota you can deploy application otherwise, you can request for more quota - -
-
-InsufficientQuota - -- Check if you have sufficient quota available in your subscription before deployment. -- To verify, refer to the [Quota Check documentation](./QuotaCheck.md) for details. - -
- -
-LinkedInvalidPropertyId/ ResourceNotFound/DeploymentOutputEvaluationFailed/ CanNotRestoreANonExistingResource - -- Before using any resource ID, ensure it follows the correct format. -- Verify that the resource ID you are passing actually exists. -- Make sure there are no typos in the resource ID. -- Verify that the provisioning state of the existing resource is `Succeeded` by running the following command to avoid this error while deployment or restoring the resource. - - ``` - az resource show --ids --query "properties.provisioningState" - ``` - -- Sample Resource IDs format - - Log Analytics Workspace Resource ID - ``` - /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.OperationalInsights/workspaces/{workspaceName} - ``` - - Azure AI Foundry Project Resource ID - ``` - /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.MachineLearningServices/workspaces/{name} - ``` -- For more information refer [Resource Not Found errors solutions](https://learn.microsoft.com/en-us/azure/azure-resource-manager/troubleshooting/error-not-found?tabs=bicep) - -
- -
-ResourceNameInvalid - -- Ensure the resource name is within the allowed length and naming rules defined for that specific resource type, you can refer [Resource Naming Convention](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/resource-name-rules) document. - -
- -
-ServiceUnavailable/ResourceNotFound - -- Regions are restricted to guarantee compatibility with paired regions and replica locations for data redundancy and failover scenarios based on articles [Azure regions list](https://learn.microsoft.com/en-us/azure/reliability/regions-list) and [Reliability in Azure Cosmos DB for NoSQL](https://learn.microsoft.com/en-us/azure/reliability/reliability-cosmos-db-nosql). - -- You can request more quota for Cosmos DB, refer [Quota Request](https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/create-support-request-quota-increase) Documentation - -
- -
-Workspace Name - InvalidParameter - -To avoid this errors in workspace ID follow below rules. - -1. Must start and end with an alphanumeric character (letter or number). -2. Allowed characters: - `a–z` - `0–9` - `- (hyphen)` -3. Cannot start or end with a hyphen -. -4. No spaces, underscores (\_), periods (.), or special characters. -5. Must be unique within the Azure region & subscription. -6. Length: 3–33 characters (for AML workspaces). -
- -
-BadRequest: Dns record under zone Document is already taken - -This error can occur only when user hardcoding the CosmosDB Service name. To avoid this you can try few below suggestions. - -- Verify resource names are globally unique. -- If you already created an account/resource with same name in another subscription or resource group, check and delete it before reusing the name. -- By default in this template we are using unique prefix with every resource/account name to avoid this kind for errors. -
- -
-NetcfgSubnetRangeOutsideVnet - -- Ensure the subnet’s IP address range falls within the virtual network’s address space. -- Always validate that the subnet CIDR block is a subset of the VNet range. -- For Azure Bastion, the AzureBastionSubnet must be at least /27. -- Confirm that the AzureBastionSubnet is deployed inside the VNet. -
- -
-DisableExport_PublicNetworkAccessMustBeDisabled - -- Check container source: Confirm whether the deployment is using a Docker image or Azure Container Registry (ACR). -- Verify ACR configuration: If ACR is included, review its settings to ensure they comply with Azure requirements. -- Check export settings: If export is disabled in ACR, make sure public network access is also disabled. -- Dedeploy after fix: Correct the configuration and redeploy. This will prevent the Conflict error during deployment. -- For more information refer [ACR Data Loss Prevention](https://learn.microsoft.com/en-us/azure/container-registry/data-loss-prevention) document. -
- -
-AccountProvisioningStateInvalid - -- The AccountProvisioningStateInvalid error occurs when you try to use resources while they are still in the Accepted provisioning state. -- This means the deployment has not yet fully completed. -- To avoid this error, wait until the provisioning state changes to Succeeded. -- Only use the resources once the deployment is fully completed. -
- -
-VaultNameNotValid - -In this template Vault name will be unique everytime, but if you are trying to hard code the name then please make sure below points. - -1. Check name length - - Ensure the Key Vault name is between 3 and 24 characters. -2. Validate allowed characters - - The name can only contain letters (a–z, A–Z) and numbers (0–9). - - Hyphens are allowed, but not at the beginning or end, and not consecutive (--). -3. Ensure proper start and end - - The name must start with a letter. - - The name must end with a letter or digit (not a hyphen). -4. Test with a new name - - - Example of a valid vault name: - ✅ cartersaikeyvault1 ✅ securevaultdemo ✅ kv-project123 - -
- -
-DeploymentCanceled - -There might be multiple reasons for this error you can follow below steps to troubleshoot. - -1. Check deployment history - - Go to Azure Portal → Resource Group → Deployments. - - Look at the detailed error message for the deployment that was canceled — this will show which resource failed and why. -2. Identify the root cause - - A DeploymentCanceled usually means: - - A dependent resource failed to deploy. - - A validation error occurred earlier. - - A manual cancellation was triggered. - - Expand the failed deployment logs for inner error messages. -3. Validate your template (ARM/Bicep) - Run: - ``` - az deployment group validate --resource-group --template-file main.bicep - ``` -4. Check resource limits/quotas - - Ensure you have not exceeded quotas (vCPUs, IPs, storage accounts, etc.), which can silently cause cancellation. -5. Fix the failed dependency - - If a specific resource shows BadRequest, Conflict, or ValidationError, resolve that first. - - Re-run the deployment after fixing the root cause. -6. Retry deployment -Once corrected, redeploy with: -`az deployment group create --resource-group --template-file main.bicep` -Essentially: DeploymentCanceled itself is just a wrapper error — you need to check inner errors in the deployment logs to find the actual failure. -
- -
-LocationNotAvailableForResourceType - -- You may encounter a LocationNotAvailableForResourceType error if you set the secondary location to 'Australia Central' in the main.bicep file. -- This happens because 'Australia Central' is not a supported region for that resource type. -- Always refer to the README file or Azure documentation to check the list of supported regions. -- Update the deployment with a valid supported region to resolve the issue. - -
- -
-InvalidResourceLocation - -- You may encounter an InvalidResourceLocation error if you change the region for Cosmos DB or the Storage Account (secondary location) multiple times in the main.bicep file and redeploy. -- Azure resources like Cosmos DB and Storage Accounts do not support changing regions after deployment. -- If you need to change the region again, first delete the existing deployment. -- Then redeploy the resources with the updated region configuration. - -
- -
-DeploymentActive - -- This issue occurs when a deployment is already in progress and another deployment is triggered in the same resource group, causing a DeploymentActive error. -- Cancel the ongoing deployment before starting a new one. -- Do not initiate a new deployment in the same resource group until the previous one is completed. -
- -
-ResourceOperationFailure/ProvisioningDisabled - -- This error occurs when provisioning of a resource is restricted in the selected region. - It usually happens because the service is not available in that region or provisioning has been temporarily disabled. - -- Regions are restricted to guarantee compatibility with paired regions and replica locations for data redundancy and failover scenarios based on articles [Azure regions list](https://learn.microsoft.com/en-us/azure/reliability/regions-list) and [Reliability in Azure Cosmos DB for NoSQL](https://learn.microsoft.com/en-us/azure/reliability/reliability-cosmos-db-nosql). - -- If you need to use the same region, you can request a quota or provisioning exception. - Refer to [Quota Request](https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/create-support-request-quota-increase) for more details. - -
- -
-MaxNumberOfRegionalEnvironmentsInSubExceeded - -- This error occurs when you try to create more than the allowed number of **Azure Container App Environments (ACA Environments)** in the same region for a subscription. -- For example, in **Sweden Central**, only **1 Container App Environment** is allowed per subscription. - -The subscription 'xxxx-xxxx' cannot have more than 1 Container App Environments in Sweden Central. - -- To fix this, you can: - - Deploy the Container App Environment in a **different region**, OR - - Request a quota increase via Azure Support → [Quota Increase Request](https://go.microsoft.com/fwlink/?linkid=2208872) - -
- -
-Unauthorized - Operation cannot be completed without additional quota - -- You can check your quota usage using `az vm list-usage`. - - ``` - az vm list-usage --location "" -o table - ``` - -- To Request more quota refer [VM Quota Request](https://techcommunity.microsoft.com/blog/startupsatmicrosoftblog/how-to-increase-quota-for-specific-types-of-azure-virtual-machines/3792394). - -
- -
ParentResourceNotfound - - -- You can refer to the [Parent Resource Not found](https://learn.microsoft.com/en-us/azure/azure-resource-manager/troubleshooting/error-parent-resource?tabs=bicep) documentation if you encounter this error. - -
- -
ResourceProviderError - -- This error occurs when the resource provider is not registered in your subscription. -- To register it, refer to [Register Resource Provider](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/resource-providers-and-types) documentation. - -
- -
-Conflict - Cannot use the SKU Basic with File Change Audit for site. - -- This error happens because File Change Audit logs aren’t supported on Basic SKU App Service Plans. - -- Upgrading to Premium/Isolated SKU (supports File Change Audit), or - -- Disabling File Change Audit in Diagnostic Settings if you must stay on Basic. -- Always cross-check the [supported log types](https://aka.ms/supported-log-types) - before adding diagnostic logs to your Bicep templates. - -
- -
- -AccountPropertyCannotBeUpdated - -- The property **`isHnsEnabled`** (Hierarchical Namespace for Data Lake Gen2) is **read-only** and can only be set during **storage account creation**. -- Once a storage account is created, this property **cannot be updated**. -- Trying to update it via ARM template, Bicep, CLI, or Portal will fail. - -- **Resolution** -- Create a **new storage account** with `isHnsEnabled=true` if you require hierarchical namespace. -- Migration may be needed if you already have data. -- Refer to [Storage Account Update Restrictions](https://aka.ms/storageaccountupdate) for more details. - -
- -
- -InvalidRequestContent - -- The deployment values either include values that aren't recognized, or required values are missing. -- Confirm the values for your resource type. -- You can refer to the [Invalid Request Content error documentation](https://learn.microsoft.com/en-us/azure/azure-resource-manager/troubleshooting/error-invalid-request-content). - -
- -
-ReadOnlyDisabledSubscription - -- Depending on the type of the Azure Subscription, the expiration date might have been reached. - -- You have to activate the Azure Subscription before creating any Azure resource. - -- You can refer Reactivate a disabled Azure subscription Documentation. - -
- -
- -SkuNotAvailable - -- You receive this error in the following scenarios: - - When the resource SKU you've selected, such as VM size, isn't available for a location or zone. - - If you're deploying an Azure Spot VM or Spot scale set instance, there isn't any capacity for Azure Spot in this location. For more information, see Spot error messages. - -
- -
-CrossTenantDeploymentNotPermitted - -- **Check tenant match:** - Ensure your deployment identity (user/SP) and the target resource group are in the same tenant. - - ```bash - az account show - az group show --name - ``` - -- **Verify pipeline/service principal:** - If using CI/CD, confirm that the service principal belongs to the same tenant and has permissions on the resource group. - -- **Avoid cross-tenant references:** - Make sure your Bicep doesn’t reference subscriptions, resource groups, or resources in another tenant. - -- **Test minimal deployment:** - Deploy a simple resource to the same resource group to confirm that identity and tenant are correct. - -- **Guest/external accounts:** - Avoid using guest users from other tenants; use native accounts or SPs in the tenant. - -
- -
-RequestDisallowedByPolicy - -- This typically indicates that an Azure Policy is preventing the requested action due to policy restrictions in your subscription. -- For more details and guidance on resolving this issue, please refer to the official Microsoft documentation: [RequestDisallowedByPolicy](https://learn.microsoft.com/en-us/troubleshoot/azure/azure-kubernetes/create-upgrade-delete/error-code-requestdisallowedbypolicy) - -
- -
-FlagMustBeSetForRestore/NameUnavailable/CustomDomainInUse - -- This error occurs when you try to deploy a Cognitive Services resource that was soft-deleted earlier. -- Azure requires you to explicitly set the `restore` flag to `true` if you want to recover the soft-deleted resource. -- If you don’t want to restore the resource, you must purge the deleted resource first before redeploying. - -**Example causes:** - -- Trying to redeploy a Cognitive Services account with the same name as a previously deleted one. -- The deleted resource still exists in a soft-delete retention state. - -**How to fix:** - -1. If you want to restore → add `"restore": true` in your template properties. -2. If you want a fresh deployment → purge the resource using: - ```bash - az cognitiveservices account purge \ - --name \ - --resource-group \ - --location - ``` - -- For more details, refer to [Soft delete and resource restore.](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/delete-resource-group?tabs=azure-powershell) - -
- -
-PrincipalNotFound - -- This error occurs when the principal ID (Service Principal, User, or Group) specified in a role assignment or deployment does not exist in the Azure Active Directory tenant. -- It can also happen due to replication delays right after creating a new principal. - -**Example causes:** - -- The specified Object ID is invalid or belongs to another tenant. -- The principal was recently created, but Azure AD has not yet replicated it. -- Attempting to assign a role to a non-existing or deleted Service Principal/User/Group. - -**How to fix:** - -1. Verify that the principal ID is correct and exists in the same directory/tenant. - ```bash - az ad sp show --id - ``` -2. If the principal was just created, wait a few minutes and retry. -3. Explicitly set the principalType property (ServicePrincipal, User, or Group) in your ARM/Bicep template to avoid replication delays. -4. If the principal does not exist, create it again before assigning roles. - -- For more details, see [Azure PrincipalType documentation](https://learn.microsoft.com/en-us/azure/role-based-access-control/troubleshooting?tabs=bicep) - -
- -
- -RedundancyConfigurationNotAvailableInRegion - -- This issue happens when you try to create a Storage Account with a redundancy configuration (e.g., Standard_GRS) that is not supported in the selected Azure region. - -- Example: Creating a storage account with GRS in italynorth will fail with this error. - - ``` - az storage account create -n mystorageacct123 -g myResourceGroup -l italynorth --sku Standard_GRS --kind StorageV2 - - ``` - -- To check supported SKUs for your region: - ``` - az storage account list-skus -l italynorth -o table - ``` -- Use a supported redundancy option (e.g., Standard_LRS) in the same region Or deploy the Storage Account in a region that supports your chosen redundancy. For more details, refer to [Azure Storage redundancy documentation.](https://learn.microsoft.com/en-us/azure/storage/common/storage-redundancy?utm_source=chatgpt.com) - -
- -
- -DeploymentNotFound - -- This issue occurs when the user deletes a previous deployment along with the resource group (RG), and then redeploys the same RG with the same environment name but in a different location. - -- To avoid the DeploymentNotFound error, do not change the location when redeploying a deleted RG, or Use new names for the RG and environment during redeployment. - -
- -
- -DeploymentCanceled(user.canceled) -- Indicates that the deployment was manually canceled by the user (Portal, CLI, or pipeline). - -- Check deployment history and logs to confirm who/when it was canceled. - -- If accidental, retry the deployment. - -- For pipelines, ensure no automation or timeout is triggering cancellation. - -- Use deployment locks or retry logic to prevent accidental cancellations. - -
- -
- -ResourceGroupDeletionTimeout - -- Some resources in the resource group may be stuck deleting or have dependencies; check RG resources and status. - -- Ensure no resource locks or Azure Policies are blocking deletion. - -- Retry deletion via CLI/PowerShell (```az group delete --name --yes --no-wait```). - -- Check Activity Log to identify failing resources; escalate to Azure Support if deletion is stuck. - -
- -
- -BadRequest - DatabaseAccount is in a failed provisioning state because the previous attempt to create it was not successful - -- This error occurs when a user attempts to redeploy a resource that previously failed to provision. - -- To resolve the issue, delete the failed deployment first, then start a new deployment. - -- For guidance on deleting a resource from a Resource Group, refer to the following link: [Delete an Azure Cosmos DB account](https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/manage-with-powershell#delete-account:~:text=%3A%24enableMultiMaster-,Delete%20an%20Azure%20Cosmos%20DB%20account,-This%20command%20deletes) - -
- -
- -SpecialFeatureOrQuotaIdRequired - -- This error occurs when your subscription does not have access to certain Azure OpenAI models. -- Example error message: - -SpecialFeatureOrQuotaIdRequired: The current subscription does not have access to this model 'Format:OpenAI,Name:o3,Version:2025-04-16'. -- Resolution: -To gain access, submit a request using the official form: - -[👉 Azure OpenAI Model Access Request](https://customervoice.microsoft.com/Pages/ResponsePage.aspx?id=v4j5cvGGr0GRqy180BHbR7en2Ais5pxKtso_Pz4b1_xUQ1VGQUEzRlBIMVU2UFlHSFpSNkpOR0paRSQlQCN0PWcu) - - -You’ll need to use this form if you require access to the following restricted models: - - gpt-5 - - o3 - - o3-pro - - deep research - - reasoning summary - - gpt-image-1 - - Once your request is approved, redeploy your resources. - -
- -
- -Error During deployment +## ⚡ Most Frequently Encountered Errors + +| Error Code | Common Cause | Full Details | +|------------|--------------|--------------| +| **InsufficientQuota** | Not enough quota available in subscription | [View Solution](#quota--capacity-limitations) | +| **MissingSubscriptionRegistration** | Required feature not registered in subscription | [View Solution](#subscription--access-issues) | +| **ResourceGroupNotFound** | RG doesn't exist or using old .env file | [View Solution](#resource-group--deployment-management) | +| **DeploymentModelNotSupported** | Model not available in selected region | [View Solution](#regional--location-issues) | +| **DeploymentNotFound** | Deployment record not found or was deleted | [View Solution](#resource-group--deployment-management) | +| **ResourceNotFound** | Resource does not exist or cannot be found | [View Solution](#resource-identification--references) | +| **SpecialFeatureOrQuotaIdRequired** | Subscription lacks access to specific model | [View Solution](#subscription--access-issues) | +| **ContainerAppOperationError** | Improperly built container image | [View Solution](#miscellaneous) | +| **ServiceUnavailable** | Service not available in selected region | [View Solution](#regional--location-issues) | +| **BadRequest - DatabaseAccount is in a failed provisioning state** | Previous deployment failed | [View Solution](#resource-state--provisioning) | +| **Unauthorized - Operation cannot be completed
without additional quota** | Insufficient quota for requested operation | [View Solution](#subscription--access-issues) | +| **ResourceGroupBeingDeleted** | Resource group deletion in progress | [View Solution](#resource-group--deployment-management) | +| **FlagMustBeSetForRestore** | Soft-deleted resource requires restore flag or purge | [View Solution](#miscellaneous) | +| **ParentResourceNotFound** | Parent resource does not exist or cannot be found | [View Solution](#resource-identification--references) | +| **AccountProvisioningStateInvalid** | Resource used before provisioning completed | [View Solution](#resource-state--provisioning) | +| **InternalSubscriptionIsOverQuotaForSku** | Subscription quota exceeded for the requested SKU | [View Solution](#quota--capacity-limitations) | +| **InvalidResourceGroup** | Invalid resource group configuration | [View Solution](#resource-group--deployment-management) | +| **RequestDisallowedByPolicy** | Azure Policy blocking the requested operation | [View Solution](#subscription--access-issues) | + +## 📖 Table of Contents + +- [Subscription & Access Issues](#subscription--access-issues) +- [Quota & Capacity Limitations](#quota--capacity-limitations) +- [Regional & Location Issues](#regional--location-issues) +- [Resource Naming & Validation](#resource-naming--validation) +- [Resource Identification & References](#resource-identification--references) +- [Network & Infrastructure Configuration](#network--infrastructure-configuration) +- [Configuration & Property Errors](#configuration--property-errors) +- [Resource State & Provisioning](#resource-state--provisioning) +- [Miscellaneous](#miscellaneous) + +## Subscription & Access Issues + +| Issue/Error Code | Description | Steps to Resolve | +|-----------|-------------|------------------| +| **ReadOnlyDisabledSubscription** | Subscription is disabled or in read-only state |
  • Check if you have an active subscription before starting the deployment
  • Depending on the type of the Azure Subscription, the expiration date might have been reached
  • You have to activate the Azure Subscription before creating any Azure resource
  • Refer to [Reactivate a disabled Azure subscription](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/subscription-disabled) documentation
| +| **MissingSubscriptionRegistration/
AllowBringYourOwnPublicIpAddress** | Required feature not registered in subscription | **Enable `AllowBringYourOwnPublicIpAddress` Feature**

Before deploying the resources, you may need to enable the **Bring Your Own Public IP Address** feature in Azure. This is required only once per subscription.

**Steps:**
  • Run the following command to register the feature:
    `az feature register --namespace Microsoft.Network --name AllowBringYourOwnPublicIpAddress`
  • Wait for the registration to complete. Check the status using:
    `az feature show --namespace Microsoft.Network --name AllowBringYourOwnPublicIpAddress --query properties.state`
  • The output should show: "Registered"
  • Once the feature is registered, refresh the provider:
    `az provider register --namespace Microsoft.Network`
💡 Note: Feature registration may take several minutes to complete. This needs to be done only once per Azure subscription. | +| **Unauthorized - Operation cannot be completed without additional quota** | Insufficient quota for requested operation |
  • Check your quota usage using:
    `az vm list-usage --location "" -o table`
  • To request more quota refer to [VM Quota Request](https://techcommunity.microsoft.com/blog/startupsatmicrosoftblog/how-to-increase-quota-for-specific-types-of-azure-virtual-machines/3792394)
| +| **CrossTenantDeploymentNotPermitted** | Deployment across different Azure AD tenants not allowed |
  • **Check tenant match:** Ensure your deployment identity (user/SP) and the target resource group are in the same tenant:
    `az account show`
    `az group show --name `
  • **Verify pipeline/service principal:** If using CI/CD, confirm the service principal belongs to the same tenant and has permissions on the resource group
  • **Avoid cross-tenant references:** Make sure your Bicep doesn't reference subscriptions, resource groups, or resources in another tenant
  • **Test minimal deployment:** Deploy a simple resource to the same resource group to confirm identity and tenant are correct
  • **Guest/external accounts:** Avoid using guest users from other tenants; use native accounts or SPs in the tenant
| +| **RequestDisallowedByPolicy** | Azure Policy blocking the requested operation |
  • This typically indicates that an Azure Policy is preventing the requested action due to policy restrictions in your subscription
  • For more details and guidance on resolving this issue, refer to: [RequestDisallowedByPolicy](https://learn.microsoft.com/en-us/troubleshoot/azure/azure-kubernetes/create-upgrade-delete/error-code-requestdisallowedbypolicy)
| +| **SpecialFeatureOrQuotaIdRequired** | Subscription lacks access to specific Azure OpenAI models | This error occurs when your subscription does not have access to certain Azure OpenAI models.

**Example error message:**
`SpecialFeatureOrQuotaIdRequired: The current subscription does not have access to this model 'Format:OpenAI,Name:o3,Version:2025-04-16'.`

**Resolution:**
To gain access, submit a request using the official form:
👉 [Azure OpenAI Model Access Request](https://customervoice.microsoft.com/Pages/ResponsePage.aspx?id=v4j5cvGGr0GRqy180BHbR7en2Ais5pxKtso_Pz4b1_xUQ1VGQUEzRlBIMVU2UFlHSFpSNkpOR0paRSQlQCN0PWcu)

You'll need to use this form if you require access to the following restricted models:
  • gpt-5
  • o3
  • o3-pro
  • deep research
  • reasoning summary
  • gpt-image-1
Once your request is approved, redeploy your resource. | +| **ResourceProviderError** | Resource provider not registered in subscription |
  • This error occurs when the resource provider is not registered in your subscription
  • To register it, refer to [Register Resource Provider](https://learn.microsoft.com/en-us/azure/azure-resource-manager/troubleshooting/error-register-resource-provider?tabs=azure-cli) documentation
| + +-------------------------------- + +## Quota & Capacity Limitations + +| Issue/Error Code | Description | Steps to Resolve | +|-----------------|-------------|------------------| +| **InternalSubscriptionIsOverQuotaForSku/
ManagedEnvironmentProvisioningError** | Subscription quota exceeded for the requested SKU | Quotas are applied per resource group, subscriptions, accounts, and other scopes. For example, your subscription might be configured to limit the number of vCPUs for a region. If you attempt to deploy a virtual machine with more vCPUs than the permitted amount, you receive an error that the quota was exceeded.

For PowerShell, use the `Get-AzVMUsage` cmdlet to find virtual machine quotas:
`Get-AzVMUsage -Location "West US"`

Based on available quota you can deploy application otherwise, you can request for more quota | +| **ServiceQuotaExceeded** | Free tier service quota limit reached for Azure AI Search | This error occurs when you attempt to deploy an Azure AI Search service but have already reached the **free tier quota limit** for your subscription. Each Azure subscription is limited to **one free tier Search service**.

**Example error message:**
`ServiceQuotaExceeded: Operation would exceed 'free' tier service quota. You are using 1 out of 1 'free' tier service quota.`

**Common causes:**
  • Already have a free tier Azure AI Search service in the subscription
  • Previous deployment created a free tier Search service that wasn't deleted
  • Attempting to deploy multiple environments with free tier Search services

**Resolution:**
  • **Option 1: Delete existing free tier Search service:**
    `az search service list --query "[?sku.name=='free']" -o table`
    `az search service delete --name --resource-group --yes`
  • **Option 2: Upgrade to a paid SKU:**
    Modify your Bicep/ARM template to use `basic`, `standard`, or higher SKU instead of `free`
  • **Option 3: Use existing Search service:**
    Reference the existing free tier Search service in your deployment instead of creating a new one
  • **Request quota increase:**
    Submit a support request with issue type 'Service and subscription limits (quota)' and quota type 'Search' via [Azure Quota Request](https://aka.ms/AddQuotaSubscription)

**Reference:**
  • [Azure AI Search service limits](https://learn.microsoft.com/en-us/azure/search/search-limits-quotas-capacity)
  • [Azure AI Search pricing tiers](https://learn.microsoft.com/en-us/azure/search/search-sku-tier)
| +| **InsufficientQuota** | Not enough quota available in subscription |
  • Check if you have sufficient quota available in your subscription before deployment
  • To verify, refer to the [quota_check](../docs/QuotaCheck.md) file for details
| +| **MaxNumberOfRegionalEnvironmentsInSubExceeded** | Maximum Container App Environments limit reached for region |This error occurs when you attempt to create more **Azure Container App Environments** than the regional quota limit allows for your subscription. Each Azure region has a specific limit on the number of Container App Environments that can be created per subscription.

**Common Causes:**
  • Deploying to regions with low quota limits (e.g., Sweden Central allows only 1 environment)
  • Multiple deployments without cleaning up previous environments
  • Exceeding the standard limit of 15 environments in most major regions

**Resolution:**
  • **Delete unused environments** in the target region, OR
  • **Deploy to a different region** with available capacity, OR
  • **Request quota increase** via [Azure Support](https://go.microsoft.com/fwlink/?linkid=2208872)

**Reference:**
  • [Azure Container Apps quotas](https://learn.microsoft.com/en-us/azure/container-apps/quotas)
  • [Azure subscription and service limits](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits)
| +| **SkuNotAvailable** | Requested SKU not available in selected location or zone | You receive this error in the following scenarios:
  • When the resource SKU you've selected, such as VM size, isn't available for a location or zone
  • If you're deploying an Azure Spot VM or Spot scale set instance, and there isn't any capacity for Azure Spot in this location. For more information, see Spot error messages
| +| **Conflict - No available instances to satisfy this request** | Azure App Service has insufficient capacity in the region | This error occurs when Azure App Service doesn't have enough available compute instances in the selected region to provision or scale your app.

**Common Causes:**
  • High demand in the selected region (e.g., East US, West Europe)
  • Specific SKUs experiencing capacity constraints (Free, Shared, or certain Premium tiers)
  • Multiple rapid deployments in the same region

**Resolution:**
  • **Wait and Retry** (15-30 minutes): `azd up`
  • **Deploy to a New Resource Group** (Recommended for urgent cases):
    ```
    azd down --force --purge
    azd up
    ```
  • **Try a Different Region:**
    Update region in `main.bicep` or `azure.yaml` to a less congested region (e.g., `westus2`, `centralus`, `northeurope`)
  • **Use a Different SKU/Tier:**
    If using Free/Shared tier, upgrade to Basic or Standard
    Check SKU availability: `az appservice list-locations --sku `

**Reference:** [Azure App Service Plans](https://learn.microsoft.com/en-us/azure/app-service/overview-hosting-plans) | + +-------------------------------- + +## Resource Group & Deployment Management + +| Issue/Error Code | Description | Steps to Resolve | +|-----------------|-------------|------------------| +| **ResourceGroupNotFound** | Specified resource group does not exist | **Option 1:**
  • Go to [Azure Portal](https://portal.azure.com/#home)
  • Click on **"Resource groups"** option
    ![alt text](../docs/images/AzureHomePage.png)
  • Search for the resource group in the search bar. If it exists, you can proceed
    ![alt text](../docs/images/resourcegroup1.png)

**Option 2:**
  • This error can occur if you deploy using the same .env file from a previous deployment
  • Create a new environment before redeploying:
    `azd env new `
| +| **ResourceGroupBeingDeleted** | Resource group is currently being deleted | **Steps:**
  • Go to [Azure Portal](https://portal.azure.com/#home)
  • Go to resource group option and search for targeted resource group
  • If the resource group is being deleted, you cannot use it. Create a new one or use a different resource group
| +| **DeploymentActive** | Another deployment is already in progress in this resource group |
  • This occurs when a deployment is already in progress and another deployment is triggered in the same resource group
  • Cancel the ongoing deployment before starting a new one
  • Do not initiate a new deployment until the previous one is completed
| +| **DeploymentCanceled** | Deployment was canceled before completion |
  • **Check deployment history:**
    Go to Azure Portal → Resource Group → Deployments
    Review the detailed error message
  • **Identify the root cause:**
    Dependent resource failed to deploy
    Validation error occurred
    Manual cancellation was triggered
  • **Validate template:**
    `az deployment group validate --resource-group --template-file main.bicep`
  • **Check resource limits/quotas**
  • **Fix the failed dependency**
  • **Retry deployment:**
    `az deployment group create --resource-group --template-file main.bicep`

💡 **Note:** DeploymentCanceled is a wrapper error — check inner errors in deployment logs | +| **DeploymentCanceled(user.canceled)** | User manually canceled the deployment |
  • Deployment was manually canceled by the user (Portal, CLI, or pipeline)
  • Check deployment history and logs to confirm who/when it was canceled
  • If accidental, retry the deployment
  • For pipelines, ensure no automation or timeout is triggering cancellation
  • Use deployment locks or retry logic to prevent accidental cancellations
| +| **DeploymentNotFound** | Deployment record not found or was deleted |
  • This occurs when the user deletes a previous deployment along with the resource group, then redeploys the same RG with the same environment name but in a different location
  • Do not change the location when redeploying a deleted RG, OR
  • Use new names for the RG and environment during redeployment
| +| **ResourceGroupDeletionTimeout** | Resource group deletion exceeded timeout limit |
  • Some resources may be stuck deleting or have dependencies; check RG resources and status
  • Ensure no resource locks or Azure Policies are blocking deletion
  • Retry deletion via CLI/PowerShell:
    `az group delete --name --yes --no-wait`
  • Check Activity Log to identify failing resources
  • Escalate to Azure Support if deletion is stuck
| + +-------------------------------- + +## Regional & Location Issues + +| Issue/Error Code | Description | Steps to Resolve | +|-----------------|-------------|------------------| +| **LocationNotAvailableForResourceType** | Resource type not supported in selected region | This error occurs when you attempt to deploy a resource to a region that does not support that specific resource type or SKU.

**Resolution:**
  • **Verify resource availability by region:**
    `az provider show --namespace --query "resourceTypes[?resourceType==''].locations" -o table`
  • **Check Azure Products by Region:**
    [Azure Products by Region](https://azure.microsoft.com/en-us/explore/global-infrastructure/products-by-region/)
  • **Supported regions for this deployment:**
    • `australiaeast`
    • `centralus`
    • `eastasia`
    • `eastus2`
    • `japaneast`
    • `northeurope`
    • `southeastasia`
    • `uksouth`
  • **Redeploy:**
    `azd up`
| +| **InvalidResourceLocation** | Cannot change region for already deployed resources | This error occurs when you attempt to modify the location/region of a resource that has already been deployed. Azure resources **cannot change regions** after creation.

**Resolution:**
  • **Option 1: Delete and Redeploy:**
    `azd down --force --purge`
    after purge redeploy app `azd up`
  • **Option 2: Create new environment with different region:**
    `azd env new `
    `azd env set AZURE_LOCATION `
    `azd up`
  • **Option 3: Keep existing deployment:**
    Revert configuration files to use the original region

⚠️ **Important:** Backup critical data before deleting resources.

**Reference:** [Move Azure resources across regions](https://learn.microsoft.com/en-us/azure/resource-mover/overview) | +| **ServiceUnavailable/ResourceNotFound** | Service unavailable or restricted in selected region |
  • Regions are restricted to guarantee compatibility with paired regions and replica locations for data redundancy and failover scenarios based on articles [Azure regions list](https://learn.microsoft.com/en-us/azure/reliability/regions-list) and [Azure Database for MySQL Flexible Server - Azure Regions](https://learn.microsoft.com/azure/mysql/flexible-server/overview#azure-regions)
  • You can request more quota, refer [Quota Request](https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/create-support-request-quota-increase) Documentation
| +| **ResourceOperationFailure/
ProvisioningDisabled** | Resource provisioning restricted or disabled in region |
  • This error occurs when provisioning of a resource is restricted in the selected region. It usually happens because the service is not available in that region or provisioning has been temporarily disabled
  • Regions are restricted to guarantee compatibility with paired regions and replica locations for data redundancy and failover scenarios based on articles [Azure regions list](https://learn.microsoft.com/en-us/azure/reliability/regions-list) and [Azure Database for MySQL Flexible Server - Azure Regions](https://learn.microsoft.com/azure/mysql/flexible-server/overview#azure-regions)
  • If you need to use the same region, you can request a quota or provisioning exception. Refer [Quota Request](https://docs.microsoft.com/en-us/azure/sql-database/quota-increase-request) for more details
| +| **RedundancyConfigurationNotAvailableInRegion** | Redundancy configuration not supported in selected region |
  • This issue happens when you try to create a **Storage Account** with a redundancy configuration (e.g., `Standard_GRS`) that is **not supported in the selected Azure region**
  • Example: Creating a storage account with **GRS** in **italynorth** will fail with error:
    `az storage account create -n mystorageacct123 -g myResourceGroup -l italynorth --sku Standard_GRS --kind StorageV2`
  • To check supported SKUs for your region:
    `az storage account list-skus -l italynorth -o table`
  • Use a supported redundancy option (e.g., Standard_LRS) in the same region or deploy the Storage Account in a region that supports your chosen redundancy
  • For more details, refer to [Azure Storage redundancy documentation](https://learn.microsoft.com/en-us/azure/storage/common/storage-redundancy?utm_source=chatgpt.com)
| +| **NoRegisteredProviderFound** | Unsupported API version for resource type in specified location | This error occurs when you attempt to deploy an Azure resource using an **API version that is not supported** for the specified resource type and location.

**Example error message:**
`NoRegisteredProviderFound: No registered resource provider found for location 'westeurope' and API version '2020-06-30' for type 'searchServices'. The supported api-versions are '2014-07-31-Preview, 2015-02-28, 2015-08-19, 2019-10-01-Preview, 2020-03-13, 2020-08-01, 2020-08-01-Preview, 2021-04-01-Preview, 2021-06-06-Preview, 2022-09-01, 2023-11-01, 2024-03-01-Preview, 2024-06-01-Preview, 2025-02-01-Preview, 2025-05-01'.`

**Common causes:**
  • Using an outdated or invalid API version in Bicep/ARM templates
  • Referencing an Azure Verified Module (AVM) that uses a deprecated API version
  • Copy-pasting old template code with legacy API versions
  • The API version was never valid (typo or incorrect version number)

**Resolution:**
  • **Update the API version** in your Bicep/ARM template to a supported version listed in the error message. For example, change:
    `resource searchService 'Microsoft.Search/searchServices@2020-06-30'`
    to:
    `resource searchService 'Microsoft.Search/searchServices@2025-05-01'`
  • **Check supported API versions** for a resource type:
    `az provider show --namespace Microsoft.Search --query "resourceTypes[?resourceType=='searchServices'].apiVersions" -o table`
  • **Use the latest stable API version** when possible (avoid preview versions for production)
  • **Update Azure Verified Modules (AVM)** to their latest versions if using external modules
  • **Validate your template** before deployment:
    `az deployment group validate --resource-group --template-file main.bicep`

**Reference:**
  • [Azure Resource Manager API versions](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/resource-providers-and-types)
  • [Azure AI Search REST API versions](https://learn.microsoft.com/en-us/azure/search/search-api-versions)
| + +-------------------------------- + +## Resource Naming & Validation + +| Issue/Error Code | Description | Steps to Resolve | +|-----------------|-------------|------------------| +| **ResourceNameInvalid** | Resource name violates naming convention rules |
  • Ensure the resource name is within the allowed length and naming rules defined for that specific resource type, you can refer [Resource Naming Convention](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/resource-name-rules) document
| +| **Workspace Name - InvalidParameter** | Workspace name does not meet required format | To avoid this errors in workspace ID follow below rules:
  • Must start and end with an alphanumeric character (letter or number)
  • Allowed characters: `a–z`, `0–9`, `- (hyphen)`
  • Cannot start or end with a hyphen -
  • No spaces, underscores (_), periods (.), or special characters
  • Must be unique within the Azure region & subscription
  • Length: 3–33 characters (for AML workspaces)
| +| **VaultNameNotValid** | Key Vault name does not meet naming requirements | In this template Vault name will be unique everytime, but if you trying to hard code the name then please make sure below points:
  • **Check name length** - Ensure the Key Vault name is between 3 and 24 characters
  • **Validate allowed characters** - The name can only contain letters (a–z, A–Z) and numbers (0–9). Hyphens are allowed, but not at the beginning or end, and not consecutive (--)
  • **Ensure proper start and end** - The name must start with a letter. The name must end with a letter or digit (not a hyphen)
  • **Test with a new name** - Example of a valid vault name: ✅ `cartersaikeyvault1`, ✅ `securevaultdemo`, ✅ `kv-project123`
| +| **BadRequest: Dns record under zone Document is already taken** | DNS record name already in use | This error can occur only when user hardcoding the CosmosDB Service name. To avoid this you can try few below suggestions:
  • Verify resource names are globally unique
  • If you already created an account/resource with same name in another subscription or resource group, check and delete it before reusing the name
  • By default in this template we are using unique prefix with every resource/account name to avoid this kind for errors
| + +--------------------------------- + +## Resource Identification & References + +| Issue/Error Code | Description | Steps to Resolve | +|-----------------|-------------|------------------| +| **LinkedInvalidPropertyId/
ResourceNotFound/
DeploymentOutputEvaluationFailed/
CanNotRestoreANonExistingResource/
The language expression property array index is out of bounds** | Invalid or non-existent resource ID reference |
  • Before using any resource ID, ensure it follows the correct format
  • Verify that the resource ID you are passing actually exists
  • Make sure there are no typos in the resource ID
  • Verify that the provisioning state of the existing resource is `Succeeded` by running the following command to avoid this error while deployment or restoring the resource:
    `az resource show --ids --query "properties.provisioningState"`
  • Sample Resource IDs format:
    Log Analytics Workspace Resource ID: `/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.OperationalInsights/workspaces/{workspaceName}`
    Azure AI Foundry Project Resource ID: `/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.MachineLearningServices/workspaces/{name}`
  • You may encounter the error `The language expression property array index '8' is out of bounds` if the resource ID is incomplete. Please ensure your resource ID is correct and contains all required information, as shown in sample resource IDs
  • For more information refer [Resource Not Found errors solutions](https://learn.microsoft.com/en-us/azure/azure-resource-manager/troubleshooting/error-not-found?tabs=bicep)
| +| **ParentResourceNotFound** | Parent resource does not exist or cannot be found |
  • You can refer to the [Parent Resource Not found](https://learn.microsoft.com/en-us/azure/azure-resource-manager/troubleshooting/error-parent-resource?tabs=bicep) documentation if you encounter this error
| +| **PrincipalNotFound** | Principal ID does not exist in Azure AD tenant | This error occurs when the **principal ID** (Service Principal, User, or Group) specified in a role assignment or deployment does not exist in the Azure Active Directory tenant. It can also happen due to **replication delays** right after creating a new principal.

**Example causes:**
  • The specified **Object ID** is invalid or belongs to another tenant
  • The principal was recently created but Azure AD has not yet replicated it
  • Attempting to assign a role to a non-existing or deleted Service Principal/User/Group

**How to fix:**
  • Verify that the **principal ID is correct** and exists in the same directory/tenant:
    `az ad sp show --id `
  • If the principal was just created, wait a few minutes and retry
  • Explicitly set the principalType property (ServicePrincipal, User, or Group) in your ARM/Bicep template to avoid replication delays
  • If the principal does not exist, create it again before assigning roles
  • For more details, see [Azure PrincipalType documentation](https://learn.microsoft.com/en-us/azure/role-based-access-control/troubleshooting?tabs=bicep)
| +| **SubscriptionDoesNotHaveServer** | Referenced SQL Server does not exist in subscription | This issue happens when you try to reference an **Azure SQL Server** (`Microsoft.Sql/servers`) that does not exist in the selected subscription.

**It can occur if:**
  • The SQL server name is typed incorrectly
  • The SQL server was **deleted** but is still being referenced
  • You are working in the **wrong subscription context**
  • The server exists in a **different subscription/tenant** where you don't have access

**Reproduce:**
Run an Azure CLI command with a non-existent server name:
`az sql db list --server sql-doesnotexist --resource-group myResourceGroup`
or
`az sql server show --name sql-caqfrhxr4i3hyj --resource-group myResourceGroup`

**Resolution:**
  • Verify the SQL Server name exists in your subscription:
    `az sql server list --output table`
  • Make sure you are targeting the correct subscription:
    `az account show`
    `az account set --subscription `
  • If the server was deleted, either restore it (if possible) or update references to use a valid existing server
| + +--------------------------------- + +## Network & Infrastructure Configuration + +| Issue/Error Code | Description | Steps to Resolve | +|-----------------|-------------|------------------| +| **NetcfgSubnetRangeOutsideVnet** | Subnet IP range outside virtual network address space |
  • Ensure the subnet's IP address range falls within the virtual network's address space
  • Always validate that the subnet CIDR block is a subset of the VNet range
  • For Azure Bastion, the AzureBastionSubnet must be at least /27
  • Confirm that the AzureBastionSubnet is deployed inside the VNet
| +| **DisableExport_PublicNetworkAccessMustBeDisabled** | Public network access must be disabled when export is disabled |
  • **Check container source:** Confirm whether the deployment is using a Docker image or Azure Container Registry (ACR)
  • **Verify ACR configuration:** If ACR is included, review its settings to ensure they comply with Azure requirements
  • **Check export settings:** If export is disabled in ACR, make sure public network access is also disabled
  • **Redeploy after fix:** Correct the configuration and redeploy. This will prevent the Conflict error during deployment
  • For more information refer [ACR Data Loss Prevention](https://learn.microsoft.com/en-us/azure/container-registry/data-loss-prevention) document
| -- Attempt: 1st (EXP deployment) For the Error: 503 Service Temporarily Unavailable: If you encounter this error during EXP deployment, first verify whether your deployment completed successfully. If the deployment failed, review the activity logs or error messages for more details about the failure. Address any identified issues, then proceed to start a fresh deployment. +--------------------------------- -![alt text](./images/troubleshooting/503.png) +## Configuration & Property Errors -Begin a new deployment attempt: +| Issue/Error Code | Description | Steps to Resolve | +|-----------------|-------------|------------------| +| **InvalidRequestContent** | Deployment contains unrecognized or missing required values |
  • The deployment values either include values that aren't recognized, or required values are missing. Confirm the values for your resource type
  • You can refer [Invalid Request Content error](https://learn.microsoft.com/en-us/azure/azure-resource-manager/troubleshooting/common-deployment-errors#:~:text=InvalidRequestContent,Template%20reference) documentation
| +| **Conflict - Cannot use the SKU Basic with File Change Audit for site** | File Change Audit not supported on Basic SKU |
  • This error happens because File Change Audit logs aren't supported on Basic SKU App Service Plans
  • Upgrading to Premium/Isolated SKU (supports File Change Audit), or
  • Disabling File Change Audit in Diagnostic Settings if you must stay on Basic
  • Always cross-check the [supported log types](https://aka.ms/supported-log-types) before adding diagnostic logs to your Bicep templates
| +| **AccountPropertyCannotBeUpdated** | Read-only property cannot be modified after creation | The property **`isHnsEnabled`** (Hierarchical Namespace for Data Lake Gen2) is **read-only** and can only be set during **storage account creation**. Once a storage account is created, this property **cannot be updated**. Trying to update it via ARM template, Bicep, CLI, or Portal will fail.

**Resolution:**
  • Create a **new storage account** with `isHnsEnabled=true` if you require hierarchical namespace
  • Migration may be needed if you already have data
  • Refer to [Storage Account Update Restrictions](https://aka.ms/storageaccountupdate) for more details
| +| **Conflict - Local authentication is disabled** | App Configuration store has local authentication disabled but application is using local auth mode | This error occurs when your Azure App Configuration store has **local authentication disabled** (`disableLocalAuth: true`) but your application is trying to access it using **connection strings or access keys** instead of **Azure AD/Managed Identity authentication**.

**Example error message:**
`The operation cannot be performed because the configuration store is using local authentication mode and local authentication is disabled. To enable access to data plane resources while local authentication is disabled, please use pass-through authentication mode.`

**Common causes:**
  • App Configuration store deployed with `disableLocalAuth: true` for security compliance
  • Application code using connection strings instead of Managed Identity
  • SDK client initialized with access keys rather than `DefaultAzureCredential`

**Resolution:**
  • **Option 1: Update application to use Managed Identity (Recommended)**
    ```python
    from azure.identity import DefaultAzureCredential
    from azure.appconfiguration import AzureAppConfigurationClient

    credential = DefaultAzureCredential()
    client = AzureAppConfigurationClient(
    endpoint="https://your-appconfig.azconfig.io",
    credential=credential
    )
    ```
  • **Option 2: Re-enable local authentication (Not recommended for production)**
    Set `disableLocalAuth: false` in your Bicep/ARM template
  • **Ensure proper RBAC assignment:** Verify that the Managed Identity has `App Configuration Data Reader` or `App Configuration Data Owner` role assigned

**Reference:**
  • [Disable local authentication in Azure App Configuration](https://learn.microsoft.com/en-us/azure/azure-app-configuration/howto-disable-access-key-authentication)
  • [Use Managed Identities to access App Configuration](https://learn.microsoft.com/en-us/azure/azure-app-configuration/howto-integrate-azure-managed-service-identity)
| -- Attempt 2 and 3 (EXP deployment). If none of the files were uploaded after running the sample command and all uploads failed, follow these -![alt text](./images/troubleshooting/503_1.png) +---------------------------------- -![alt text](./images/troubleshooting/503_2.png) +## Resource State & Provisioning -![alt text](./images/troubleshooting/503_3.png) +| Issue/Error Code | Description | Steps to Resolve | +|-----------------|-------------|------------------| +| **AccountProvisioningStateInvalid** | Resource used before provisioning completed |
  • The AccountProvisioningStateInvalid error occurs when you try to use resources while they are still in the Accepted provisioning state
  • This means the deployment has not yet fully completed
  • To avoid this error, wait until the provisioning state changes to Succeeded
  • Only use the resources once the deployment is fully completed
| +| **BadRequest - DatabaseAccount is in a failed provisioning state because the previous attempt to create it was not successful** | Database account failed to provision previously |
  • This error occurs when a user attempts to redeploy a resource that previously failed to provision
  • To resolve the issue, delete the failed deployment first, then start a new deployment
  • For guidance on deleting a resource from a Resource Group, refer to the following link: [Delete an Azure Cosmos DB account](https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/manage-with-powershell#delete-account:~:text=%3A%24enableMultiMaster-,Delete%20an%20Azure%20Cosmos%20DB%20account,-This%20command%20deletes)
| +| **ServiceDeleting** | Cannot provision service because deletion is still in progress | This error occurs when you attempt to create an Azure Search service with the same name as one that is currently being deleted. Azure Search services have a **soft-delete period** during which the service name remains reserved.

**Common causes:**
  • Deleting a Search service and immediately trying to recreate it with the same name
  • Rapid redeployments using the same service name in Bicep/ARM templates
  • The deletion operation is asynchronous and takes several minutes to complete

**Resolution:**
  • **Wait for deletion to complete** (10-15 minutes) before redeploying
  • **Use a different service name** - append timestamp or unique identifier to the name
  • **Implement retry logic** with exponential backoff as suggested in the error message
  • **Check deletion status** before recreating:
    `az search service show --name --resource-group `
  • For Bicep deployments, ensure your naming strategy includes unique suffixes to avoid conflicts
  • For more details, refer to [Azure Search service limits](https://learn.microsoft.com/en-us/azure/search/search-limits-quotas-capacity)
| +| **FailedIdentityOperation / ManagedEnvironmentScheduledForDelete** | Identity operation failed due to pending delete or resource conflict | This error occurs when you attempt to create or update an Azure Container Apps Managed Environment while it has a **pending delete operation** or the resource already exists in a conflicting state.

**Example error messages:**
`FailedIdentityOperation: Identity operation for resource failed with error 'Failed to perform resource identity operation. Status: 'Conflict'. Response: 'Request specified that resource is new, but resource already exists. This may be due to a pending delete operation, try again later.'`

`ManagedEnvironmentScheduledForDelete: The environment 'cae-xxx' is under deletion. Please retry the creation with new name or wait for the deletion completed.`

**Common causes:**
  • Deleting a Container Apps Environment and immediately trying to recreate it with the same name
  • Rapid redeployments using `azd up` without waiting for previous cleanup
  • Resource group deletion in progress while attempting to redeploy
  • Previous deployment failed or was canceled, leaving resources in an inconsistent state
  • Concurrent deployments targeting the same resources

**Resolution:**
  • **Wait for deletion to complete** (5-15 minutes) before redeploying:
    `az containerapp env show --name --resource-group --query "properties.provisioningState"`
  • **Check environment status:** If status is `ScheduledForDelete` or `Deleting`, wait for it to complete
  • **Use a new environment name:** Create a new environment with a different name or use a new resource group:
    `azd env new `
    `azd up`
  • **Force delete and wait:** If the environment is stuck, try force deletion:
    `az containerapp env delete --name --resource-group --yes`
    Wait for deletion to complete before redeploying
  • **Delete associated Container Apps first:** If the environment has apps, delete them before the environment:
    `az containerapp list --environment --resource-group -o table`
    `az containerapp delete --name --resource-group --yes`
  • **Use unique naming:** Implement timestamp or unique suffix in your naming strategy to avoid conflicts

**Reference:**
  • [Azure Container Apps troubleshooting](https://learn.microsoft.com/en-us/azure/container-apps/troubleshooting)
  • [Manage Container Apps environments](https://learn.microsoft.com/en-us/azure/container-apps/environment)
| +| **BadRequest - Parent account does not provision correctly** | Parent AI Services/Cognitive Services account failed to provision | This error occurs when a **child resource** (such as an AI project, model deployment, or other dependent resource) attempts to be created on a **parent Cognitive Services/AI Services account** that has **failed to provision** or is in an incomplete state.

**Example error message:**
`Parent account does not provision correctly, please retry creating the account.`

**Common causes:**
  • Parent AI Services account provisioning failed due to quota, region, or configuration issues
  • Using `restore: true` flag when no soft-deleted resource exists to restore
  • Network or transient errors during parent account creation
  • Invalid configuration on the parent account (e.g., invalid SKU, unsupported region)
  • Previous deployment of the parent account was interrupted or canceled

**Resolution:**
  • **Check parent account status:**
    `az cognitiveservices account show --name --resource-group --query "properties.provisioningState"`
  • **Delete failed parent account and redeploy:**
    `az cognitiveservices account delete --name --resource-group `
    Then run: `azd up`
  • **If using restore flag incorrectly:** Ensure `restore: false` in your Bicep template unless you specifically need to restore a soft-deleted resource
  • **Check for soft-deleted resources:**
    `az cognitiveservices account list-deleted`
  • **Purge soft-deleted resources if needed:**
    `az cognitiveservices account purge --name --resource-group --location `
  • **Verify quota and region availability:** Ensure you have sufficient quota and the service is available in your selected region

**Reference:**
  • [Manage Cognitive Services accounts](https://learn.microsoft.com/en-us/azure/ai-services/manage-resources)
  • [Recover deleted Cognitive Services resources](https://learn.microsoft.com/en-us/azure/ai-services/recover-purge-resources)
| +--------------------------------- -- Troubleshooting steps: +## Miscellaneous - - Review the error messages to identify the cause of the upload failures. - - Check the status of the resource group and confirm whether AKS is running or stopped. - - If AKS is stopped, try restarting the AKS service. - - Attempt the file upload process again using your script. - - If uploads continue to fail after these steps, proceed to start a completely new deployment. +| Issue/Error Code | Description | Steps to Resolve | +|-------------|-------------|------------------| +| **DeploymentModelNotSupported/
ServiceModelDeprecated/
InvalidResourceProperties** | Model not supported or deprecated in selected region |
  • The updated model may not be supported in the selected region. Please verify its availability in the [Azure AI Foundry models](https://learn.microsoft.com/en-us/azure/ai-foundry/openai/concepts/models?tabs=global-standard%2Cstandard-chat-completions) document
| +| **FlagMustBeSetForRestore/
NameUnavailable/
CustomDomainInUse** | Soft-deleted resource requires restore flag or purge | This error occurs when you try to deploy a Cognitive Services resource that was **soft-deleted** earlier. Azure requires you to explicitly set the **`restore` flag** to `true` if you want to recover the soft-deleted resource. If you don't want to restore the resource, you must **purge the deleted resource** first before redeploying.

**Example causes:**
  • Trying to redeploy a Cognitive Services account with the same name as a previously deleted one
  • The deleted resource still exists in a **soft-delete retention state**

**How to fix:**
  • If you want to restore → add `"restore": true` in your template properties
  • If you want a fresh deployment → purge the resource using:
    `az cognitiveservices account purge --name --resource-group --location `
  • For more details, refer to [Soft delete and resource restore](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/delete-resource-group?tabs=azure-powershell)
| +| **LinkedAuthorizationFailed** | Service principal lacks permission to use a linked resource required for deployment | This error occurs when a service principal doesn't have permission to perform an action on a linked resource that is required for the operation (e.g., cluster creation).

**Common causes:**
  • The service principal has permission on the primary resource but lacks permission on the linked scope
  • Missing role assignment for operations like `Microsoft.Network/ddosProtectionPlans/join/action`

**Resolution:**
  • Identify the **service principal**, **resource**, and **operation** from the error message
  • Grant the service principal the required permissions on the linked resource
  • Use [Assign Azure roles using the Azure portal](https://learn.microsoft.com/en-us/azure/role-based-access-control/role-assignments-portal) to add the role assignment
  • For more details, refer to [LinkedAuthorizationFailed error](https://learn.microsoft.com/en-us/troubleshoot/azure/azure-kubernetes/error-codes/linkedauthorizationfailed-error)
| +| **ContainerOperationFailure** | Container image or storage resource does not exist | This error occurs when an operation fails because the **specified container resource does not exist**. This can happen with Azure Container Registry images or Azure Storage blob containers.

**Example error message:**
`ContainerOperationFailure: The specified resource does not exist. RequestId:xxxxx Time:xxxxx`

**Common causes:**
  • **Invalid container image tag:** The specified image tag does not exist in the container registry
  • **Non-existent container registry:** The container registry endpoint is incorrect or inaccessible
  • **Missing blob container:** The storage blob container referenced by the application does not exist
  • **Incorrect storage account URL:** The storage account endpoint is misconfigured
  • **Permission issues:** The managed identity lacks permissions to access the container registry or storage account

**Resolution:**
  • **Verify container image exists:**
    `az acr repository show-tags --name --repository `
  • **Check image tag in deployment:** Ensure the `imageTag` parameter matches an existing tag in the registry
  • **Verify storage containers exist:**
    `az storage container list --account-name --auth-mode login`
  • **Check role assignments:** Ensure the Container App's managed identity has `AcrPull` role on the container registry and `Storage Blob Data Contributor` role on the storage account
  • **Verify storage account URL:** Ensure `APP_STORAGE_BLOB_URL` and `APP_STORAGE_QUEUE_URL` in App Configuration point to the correct storage account

**Reference:**
  • [Azure Container Registry troubleshooting](https://learn.microsoft.com/en-us/azure/container-registry/container-registry-troubleshoot-login)
  • [Azure Storage troubleshooting](https://learn.microsoft.com/en-us/azure/storage/common/storage-troubleshoot-common-errors)
| - ![alt text](./images/troubleshooting/503_4.png) -
+--------------------------------- 💡 Note: If you encounter any other issues, you can refer to the [Common Deployment Errors](https://learn.microsoft.com/en-us/azure/azure-resource-manager/troubleshooting/common-deployment-errors) documentation. -If the problem persists, you can also raise an bug in our [Github Issues](https://github.com/microsoft/Document-Knowledge-Mining-Solution-Accelerator/issues) for further support. \ No newline at end of file +If the problem persists, you can also raise an bug in our [Document Knowledge Generation Github Issues](https://github.com/microsoft/Document-Knowledge-Mining-Solution-Accelerator/issues) for further support. diff --git a/docs/images/AzureHomePage.png b/docs/images/AzureHomePage.png new file mode 100644 index 0000000000000000000000000000000000000000..801510ec40cdb9c88c31b0f748e90146bbf505c4 GIT binary patch literal 89286 zcmZ^~1z1&G^FEA-q|zacNOvO*l80`jmG16F1f&!W9J;%^TNI=_q&p7X9pC2h$@lmD zx%PF~9CoZVGi%n&J@tkOLGXcrc{+j!3w)FEEA<)%1|0?@ zCZg*0WH$xjJ+}DlPtyY?S7-!>6e2ntSP@PnQ5r`Kk){)V3ni13h7NZ(^U?qhg-cl6 zO${A~fkQ+r^t0b7cvuvK2yM}sy-S^LY0?49IQUagG(50!bMu|_v$MCQ%r@N|uqf4Z zJ%tm7`R|cVi@7zL)Azz{{DnV>fCon-Z2)Qj45A-!2n%2-vhx4?hV2v>0=OsWF#aB7 z=@S+^88}l`uvWS!&4%M-FtEVENe%;Tkf?t@XN2_r{qL{-{2cy)7|A7_5%QvN`MD%I zoUQFa&=~0{9Gq~wCT#iBC+PW-u#u@d7+i_4z~ldQlvDdTrC=x)sQUZ+Q+%D&M6v6? zy}c#dHzau+;o~52;K&3to*;-nzG%q+jz#CM`~7p@l97=Km#A|?MmXl|BU{l1UtFA~ zoBkdV1T#MmR%+-Bj$q)=C;fijAM-}Hb96*`@xs~FwJf-H7!Er4>eVX}5)zFd*cy@F z@=G_z^gEs77^0VW_Bip!!>$2N5$#d91guZ=_4SqCauUeBCJ{B~YJKEaXB7H3qX`i1LU|+N(H=zT+*C-k@HoA6b;Ne0KZN_YNdCNTjC#0Q0ukC^o}v&A|6eU?6Iin0|6|5~ zYC7hWRM}q&g)!++cP@AG^C*h9ESV|oz*0~N}KjlvZRMTfr4MvamXlg5CG zXA|%YgCn28G6@4RV@zIaZx?QiadUGc67oqdWp>J7efhj*2@m-1A2LsmLKo)#(_Vjn z0|RFTCxu7u{F#GXIaG?7eByKUYi(LgvL#+%5$Qu4a+5ToqN1Fb!_hdcalZzimZTxAJHPDfD#(&m!C*d<3Wg_%k0Ff>*eL8r>ED#85#skKbaDS@1HmP zJq5Fua8i8a&qACil%okGz9uR&7vhIWcb+ML!2JCD9!_oht)A4)hqvridLH*Vc&RzU zA$X~tL2OO>Qv>t!DgE6k%7K`2+;$?G=mY&$1_oDTpQ%VFs@nTMlIbGBeYuz3ZnBy` zJhW3#7+U@L6%P-ui2?38(O(PTFCFw^wqyLUO#BRCv&o{qh*@BoOMhXE)JA}l62ldb z;#kM{Zkuwl-N#PiY4nRxG>fkq4e^kgG7*+CGiz35W0jrc?x2Vnt?djqMP5cN-+Ll9dsYV1G(;Q}C`z4CibWp;#*U@sKY@IgYQ36fP z2-%x`%eXIJz9b^@I$i@cTIk4ok3adXCw~ueKa>CQCj`h(Mx&(U$mK#}a)L(lsOHf` z(51wvGOpG&nPrphzP7db0t#?(aq;mH0fj8k2+>5P=}RT3jx&%g0b=d$H;#VJW=pTI655NYvD)nW-~;! zswTWRwhZ)>&H+v+o>-Y&SA?IgLZQ^7?DDy$ieCJT?5qOTt`67C zH>%?JsWbd&x>2-tQWdpXUdmFnvHB@rEF?X0qn%C$xcTPI7zs!FU$Pl>C-XS^+#dbh zo2|3`RsLZlQ~ayYz1w1|cY&1oZmhPA`}wpfZwznb zEPU%Q9!%;}Z$6f*Ab7Qwn`Bd{G#}cvR$>5n*wg*_CXF(^IE|?uEeiawOt&#PZdvcc zCzvrPC@2#aoW{yz^E}+#-1la;tpYda`_qKX4>;Vt+2qIs5sBdu604$Px##;6YvZQ= z+3#tq?mj1%@1QX0G0?K%>~w-LE9!sf=vXn0B%Q%4*@8m{@htl3T!m7ankMH}vd)$9 zZd~laUOem;WjgW^Xx`r5Lid-e1uA)y^(NM;TT^9=7mL0RwUW|hg^ z^-h`7)Jn~io_4Jje#qdsJm4GqVi{@e14Z)2o*^KNadv=&UQ2{u0vB%fqDem|6`bn{ zWqImPB?}`8L?M4Wir**o4zTP<@{#c zp8@OZ0kQ_n*~S$X+q4m0E3m>~+6t)+~;*?VazDKZ9Z%oVI!>v@;8LkubwoA#IZ zNdqQtFWO^zthMUMHKctQPj?HOBE_;K-%E^>iHel=SWyMe8?nWQ!&mjtB+8L@W=2U2 zf{*6~#`k$^l7h94#}oM{#QiHUn~3nQ9JYU{SWFgA-(HTKUf5`|R6MyT_jh5m zIiI3$O(mWZ_6)Z1|5uElgYo2XhcR=2^Els~F4g73)ZP=7mPV%aSXaLXBZD$rp165< zobOD1b^1D(!WVtg(a~|fH@oP4F`q4h00%d2qYZJl9LtUHj?ydBZ}Yj+_r1d*B%E#Y z^}Wf|Qb2pjE_yWW+ixhHDZxrBlg%8}tD2=XK~QHlV$kskDO*s<4Oqh8J391y@6PUS zE_@HaAuKE`4Dp{lUsu^0$wDZwnD8ii9RSG2?R(IRwugH@%I$n4hn?kRLHE%X4%w<{ zv6JXEL*y7T0&4kWbXB_1yPD$wOru!4-Cr3`gS-9uX(F#>D~pqoW_|aY;)-d4y|}K) z?1cn%hPvCX7(9%j2O=|2*=CUT3?@VMf z?hVDE4-E~a;}Rpj*{vEfBN=P50eltC3#Qk>Ovq3uR9-!MHXH$!=pC@?bQNh7Kcdo( z^=!Xb3`bekA0ko9&H8fO`*LZC4*jJ#_&C%Z4N}|B9^?5khRzpNW*`!X`15684`2Kh zNBtLfnicqJO1m;yL(s(^q1CKgsS7%XL@&Ia9`Tjv|~D$T)~|y+FLzTFwfER znZ9@Xvkp>i9Fb*JOK|3Q&EwMPqFrzKP4|bGfhy*&Qg)HWNxy;_kS_ z(;RIv!ft-1#}x|Dcs%30IG*5`u;1bIzdq$+gjEP*DL^>EVhI#EQ6KH6bX}u5r2MUH znvW!;BV#e~@j<~FFRxFbt9yGbR@0SLrb9DLFNmJD z+W)0=wPPASuBNOj@s-4TfO9G`sNVa=MVSW&sdX}G-*xu;xu7kbr~;2^n0JySuGJG_0oSHA;zC-usp03u2)OUM?kaT8^$E%)g4~veqi8 zV+th?>3fQB%1lF3F89_$a`-uq>p2l?9xgP}KpQ>2og!DgV2Cm`Yg<*?Um9e?#Kc6x zVMgYktpL11jDnm=P zxMJzO zlqU25Y?t2G(7ZDb@0x+}(kBCiqfK}W?}Vo+d-;JS5mU-0eYqB`U}i9Kmd|ELeF80% zu9|$$qH4Ve_;T(Aw*Z?nXXt2PDU^Pf#0iM%PJt~nT64mOz@wm;t?$^_T!GX=!Qzya zFQlp(v7ktT>G>~cpMAy?@#&ss(QFYso7+7a)z#H8ApZkg zfr_ldiw4SX-UkaUJ5yz({BA6?gTbi8((>}HmAVQ&MFGRPhWfK$)0}$uoiA}m7w{l` zatH|8yt0XGm?AGpA}uXVQ_33|BRQir!R4$Z?aDAyV|lS2r%yMC$Ebx(OstDH`_!qW zR>sT>5knnG$2O-ygxo<%N$JCfSCm_kAIT2MbEq-1o`Z7K*YVcIiq)<1KVv$*T*!2{ zlI0-g+9sp@9$~hRmw(|Rb2e!VtYO;o5tGC8tT*#h?BByM z(==*`<34rs5@Z>KX>uT`EaCsYR^Nzp2|#- zl0YVZl}ZD)905n4JR>7rjR^AprZ+(JO@v9t`TF(ifKR%?IWZuD#iP}q+SO(vP+sJU zii(1Qf^lhJkFj26uMzLK_J4{X<&6vHAG6inEv2uYH)*1*8!WQ9JYU-h#3~3cqjYP^2%vw=t6*9uC9x0^WymSf<0y*lP6^~x4Y>`L`Di&QCW>V|K zud!vW=Ofbwqbcr(XFVs~JS!0IPe>nssAAh35kiYD`lN;i`}-9J7^GzeTAj|{YAH)w zC`*6N&^4VOwlL2xtf4Wr12TfPvq^2IfR9u!Qt}*f&K!T0=?f{xnscd+5Dl!}8Iw-f z3@8P1=X3)}=)N-zw0Gs#uXoWsrwg73d%*T+Mu|q3$mhzG0GT~&V@tz1x#}V~HCg|5 zExEj6;@e`->7Bx#V48OmbBneyvhyCZ?mL2tL15(sGxw_6r38@!lX!1xTdaESiB=3P zf^iXQLrvw4TX`soIsWQLlaT~(T;j1xp{^f++FM#>pBPd4?$(8Dvapq`^m{F>HCgOC zU>lBO&j<@{&$+^D@Wn$~JT(Tvo_tEQu~p~17p~9f3yyn{!?2q08ibmI9p2hRN>&C# zq!HYpp&BnAih@)}z&-IfvoSSwy?RI6l^9=lwF(&0Ae|kSN;}~_x+{{XNzoVLJQ;In z<;gCB3Nf0jAtvGAm8y?l6@0XIFDVo^{F3`#yR#j7U1{bc#QVk@-}quCg+wc@vPhX&f^%x9%Jbb@87r zv{$e-8n;6NeNd=YGiYq5bSr$$fYduw#Dr>vH4#Dax zU|QcXubJ`OZwTfn0`}lI=AwXAAji17XnSY@%pxElKp};<2H5QI*-gBFeDGY(`*cJy zrmbvu6$r-9xj5x4$G34A6A7Qgcf57RIRlHCDo3hW;1Da9<0!r5$&L@7u&7Duk^_ys zdd8N1fy$HZl!dl$qgOIPqKvutK{Rf;WZ*C({v+4CM$&Ro-=S-tU6Y+Z$SWNdD?oK2 z>LVXL=mZ?f-yfK25*j8z*%I9FLzGUP&_OF7l^FWpNbtdQDt6kpS*`INUuM+X*xo<-7PaA0yD%Rv;UUJ(6 zWeO^DXYT7@c)uNtE{YTlqmrUqZufgF!yw+wq{)YjgEz`{KD;EKx?bypN>IQ4<)eQ`2tDx=F^<=o12G95`}l1#yLG(2qZ=h>rcK1MNTk>gj1Ggie2m* zZrX)u*nAbVDOtFS6HD7kdsjfy(L8)qRY_O&CHjtT1mR(WJ@(-sPNmIxH`|IS;%G+H zL0qOxeb@W=`gu;;ywGKw?gO0pmT}SHYbJ%og^gWihk<>L+vll$zvw4EWPH#ncY0W7 zFh2q^S9fx=s+F!MpOlWJPvyL{O)cIFrePg(fEN0P*yLXJdEvXJIcstZ(9E8uA0K|> zQj1@#t1nZjWlPDw&RskFhU=Z+)#twfHnw2oQC^!ipPSMz+k!AENG(`gwk!K5VQ(wz z%X}%9znGqh5_kpcUj%}&{uA;46XwNwExfH~VPRqYA`}kSSOWp9sio>xZ8M)pK(2I# z97h-(-3wbVO9$Cr>xniJX6*@#5jCfbbc&9T&vy!_#>4D|-1dbDc*8Q4WyaaQufZuO z%H~wFR4)-p6Z8gRr>ET9b!DyBg~+Qw4)5C^&q^^_7EP-5Zc-YfK+nl@$sd8l4KQ<| zyVEfs(oWb%{#DdC9;#RNaP*V3VD{WUmmWvg{46>C<_)LP3xwVpR_?2Cwq-xr<=TdT9nnUrgrL~5&l=A|EJ- z2C6IIysg@c&uZ}FC#P3`nr*Z|kI0LUqAHErZ&k)VDB&dNHo53l8i8=&CvY*IBw9}- zj=+^n20iZ%-yov2AOdsGo8IyHoN$AG5s?VANC@&zCzP4KtCp&@IQLEhYr2(9XT2oE zA!?V7W30Mj$F50~?TTXSxiBrSHf)QW1a}fy-|a>>nET1Kdppk|cpiIu%rW(S$HIGV z72UQOFgltu245^~RD!@9|K54)`xkH461uaC3HzLh>16%$wJMWktdbhyHeMy;l@5f` z3(&2xFhp+FJ&*N^=Y&iD&(6VngMllT8N7#$8y(*pdakFGtoGjmrfwqli}NR9>YQfY zpoVIc!J~w%s53bFTyv*gT^h|KM!%|VRi~1br#);Vm&zO8pHniaKeK(#CT*;7NfuNu~KLC7^d-w`h)IvIyBtI)P47u#rQ@*t`vFdaq85bX>}#+V48 zA~5u_#f@#&On4t&lF;&zej~mrX z2?@PibfsF`Yy%aP7}v|gWqmq+F53lJaihrSXwi^H#1uYPc>rsCZxdons}yB8rp|KFXWw>%~F@^T+lm&l3H4e>zrDhYcWvBS%1_;SZBL<52RgD ze%cAF1t;0z0>|C2jYR!_Z1yr((mz1+(ponXEd{T)va~d(_q@uScvHFtDjgE44RgpT zfiPr;t7LEeO|Pmny~(8SCbbe_$!l@YLWS)Qo>>GOih3t5!4(wyaDls!A$y1Jd^q<+z>b zlne#tj13C+b;n}Q^0cI_uFyHP&i%Jh!NQ@A8F|9nz(?V*ZueA`6I&_oxUfgA;|UO2 z;$mapp3mCeU7ux{?|G3Gt6gMCxcqkeo7e`{{7^25DFt)QWby4p<#gvli^A#Q2SE{Q zy1HtJs@ggPXN}r^qMq%pcETy9dq|35Aol0kfr3j-9MuVHUHJ>?+I5p+A|k!^lcMin zU#q4UVY|(;2=#bPrqkX^B$ZaVY;TeS>66M2$Aw$@g62b4E5}xo`FXUW6eKSQCwKW& zess*n(RH0iM-LTAbAd@FUiVgQbyMx!p&buylAs_$IBj^v{zMqB;fA071v~n83Hz;K zD5v#38||iv>3fcbMk?s#7RRJp>h&&r6MWL%F0F9}?vgZcH8I(M)Txb#3Jb$TtbcBv z4N~?v-5UA)8Bs_`XgzWTs69ke$RmxW1BsnjNFOS+C$H7pR9eX=_JpK6zEDwovq&d{ zl2vIkLt{DkRD~`lM8y8?(^#HDbgA^)Z6IRa-P_wMA4#%pO%OYA_;7W+jvA5D&ygg{ z@lIrE$E6^YoHROJ*1%cAM7c?I)rQ8Tr{^+!TZu$v zX4YwR0u{u^#^zGc$SZ7aGdj1&CZwdud8d+;+#2Z2rd1D3t#%qxl=O_U}a^;&a)JLq;3brj6-SujM0~FL5v( zjE&5d^%oNbuM$l{$MVoX$p~m9PVYPd6M(8Gg}2F4yMLWFDF9mLB?CZ##%b}?&c)uW z25XxH&$RhwTq}DHS)9zL33ZCcbZbHj{G>p3+)?^hU>~SdzwI8V76s z2!zk$K=Dn#+AL~d2mpTG82851*VoVK^sRVUn2qNvq46KK6WTU^U@wA*IX-mYZYL`` z>*e!Oi;3IYd`^6;S5C(*8;$qAw};0xJqS+I&^G7HW~US<+Y2*ApnkYNjz9Wzw_pWH zp%u1~Y}ze#w+NLpI<*H+F7e@}?IBA>%{V&9YMXa>ps3aRX0jTm(cWHp0X{z$$NJpK zQ>Z8xil# zU0dBHslCfBa)$Za=K$C%DXiJE$7CoG`w8Z_g)x72Y^iqL;OuM?92;pGn@pdS3^B`l zVZaT^C%=h;s0Bbe~?RnI;0SD<*TUhO29SDsEoPp$p>py3rFtehuuOu8^qVhb^!GtnEf_jNms&gqg) z!8?umlEI%kt+-U^jU@aTXlanRBdGZ3o6^f8rOQ~1OijnM1Fqknh?W>vschxF~3W}%j`u{Uk}Vb5gmW_%3?JAiSBmfVlvaw`kNSV<&A~fhwm0 zD!1)IbJK2s1C2gyj+pMJ+ud<+rp4q4Gz_J?|7sFsOkUFCenZCqI=)^BV=iP#&rD8E z21>m`=f4b{HvLd{7_)P7aImr8U{8a*4uG(xuJUVlcQ-vWi6#Uuni;GoJHvYQ>MGs< znc`SEQELrc+J1X?3z~Rtg}b%6BN-c0Fk#-~R~XIuOj<=&+{PwxZ7nP&{ksIQL-Y%b z#yJy)GW$P>skKXR0yrbi7;-r&`cu#TNe@2Tb!cd4YB8RKkm_WOX`X%U zdOzAgQ(1JLaap)h)#mi^x)9;J81F9@UlQ-mLUCdT{D>K|ovY8-)mAgk_UMa!KiIBx zFzU?C7vEmB@~o;;UZ1)d^u67o>c+yg!k?h>9f}*XBE&zrK38akX;AIkpku5e?)4=c zUicN8TK~Rpp+y__(4??Xd0L%t(MBkBNLupMgS&8&EkX!S+o4d-NA6Cdn{k$A!umGTb};0q{{nVmP17#yF92juEdcN9(wbthHC z*!(yiZU&|BTl;DZBjM#xrP-|bs@o|SFyl%_mq}>H#_@{&54e~vkJQeC_=X}946Zcl z0Rz}ebgM|_N#rY0tr`nRYHBM0k}Ue%L>vRfsF%dVd*%DarY_s#(8J~L*)1_A!aOkw z$Zid{TKuY(bFiY?YI`hX6yW~C+6rb$NJur+O)^yiRYv&uno1s>4S%o17ow{k>NGE% zb5`Hhst3^~HXTVUOewQmWpU=*c+~m1IU0HXXu#TqwD0FCop&aEU2aAFF}1mDGsIS) zVxX8gj=Au{Uxi5nE6eoSpXEvk-d=jBRn*p`|z;(yYG2b25{oPvVjiFvpbBP z;FXzEv3+AWYGZjLyfeL^ppyAn3!t2<&N`XxjbpPo%E&eXhOSVd0(UB%7NVxU`h2SaVt#c+*CG+ zispvF&XW{$eD4gy%gW4bZZ4b-O?*C6Z62MRtP@z&M_WnbL0_-RZ~&>WI2Y=PZKE}f zz6$-qa7FzdbNZ0w4Zr(X{Y~-g69lKqjk1?7?TkVg7iaJ!=}tuVfMO79utwkyHBzJJ z6yG4h)E?!&iwA=HYDGmFKb?Ia^e0!5^dyF;%ZA=fwA^-K_un7wRS$#|pY0jZqtrWg zwLI9iI@G%!Jl&O{KKCds?)~vh+h)@@ZFoC^yTY}MJj|ERxt-ZmGf7E$;puJ|+I`6} z3H7wAKE$jhPfG!gSb5y#rh-M?=W+ypc1<9wZ&M>Ir&xOyL;tJJIU2lH=*G}XZc#za zB9ldK8~CYo#%B|4#Tf)iU`B2Ku{C@myEV(Os%`qXs-=?I8!cAarC+<1^ap(t?XP|_ zb|5f%|By}6{m}t<3n;z6YjW9xy*k>fTLf^2n~NTEl`S1YMcl&jZB zCE9hlLGo#Waut|cOwU3_V)Pmu)+2C802V`fCOuLx8fi3>}nqIPSc>{6r5`cv_H@ba1|S zRf6krW(Z=QaN{3fN4P~lTtD&jQ?xk{^8}JTa$luTn zS|z9|C)yUtHmpjYneimm+2i}ZKb$pxYNbtrM$?w*UZDHIl}{~^m1zkzKm8Y&q3DfD ztzm7Ed?IJrsCRND#$9s7e3N!zA8~m$zN3zKtrGui=bDOtXN%@Bf~+}F&H-8mkmH~hC<;Pcprf51pb%5|;5x^ykZ5Gi27q?* z7?08D(!UZE6r`uOyuZCldq>JOVg(=^ll=F*7UMLSlNLEjrpY>v1DyEy`0noRMsvAx zNz+xPzCe*;O5az2cv76~LrZqH(axlGs-9jpu?iLi)7aeQ=7H}q$}z|~R}}L(ZDhTy z|8qEqaDr+~T3QqihMY<8Umo|@?0=QmPJQk`xSVkoc8crlQPTvn<7RQy^gd@nOm*lOTia($ zHA%<|zdazIEIE~}m!le!VQx9SIcRG;8RT<%m$uw=&HIxl5YW~mXS)}HwgB#;5f}zZ>Ubk z@kJhxLbS}9FedAs$zEd?$eufwvB(k3XM>^*5zVw8${ldV0cwOp-0Q^9(H@baWqHT58{; z=4(BxP>5na++YNz+9!Igv*ODf74gQC@d6dd_v>)q%VmVb;`v5rAUjwDidzDs={M(l zo`C5+vcL3wuGRr6N@ix}Mt_26or;B}&#?G6AW^ZARb%a6SuwCkyjB;#1kkhYVr1$U z6U|LrLEuW1H}V4R8qlYfMAo^A<8U7 zXH-YSz(8SNo7x20J^@w$KsqmN)&(<%(P@2g3itj3ViJVsFYR_p z>dp@qA6CB#<;qgtUao$vp^V`f6M#rhR2cqzxI179OF;Sy3-IOG22%J(TpZwI*ypZ5 zensxq8uf5nL{JGaKeYQl1yrhtBx?}p%QvYEnIGYm)%6a?{l%23LPD2IfMcjM2X)lb0boQLyVb1&$`oi>n|B${qM z7O)sD;b+V5**1P1#AKILyw{bNI;xGEdFr2DI^oiFx}?+0J&-AM+?Jc)N1v*LSb$e zPAN!>5X9!Z`q87J&v;MTGnZXOk6AV9>yHF%m#BM&yxHk%@v7u-P$e1`nNE`eF$Sh6+n5LPjekZSpswErrxpi+Y{^$MkWKik3+w2;15I%f> zbmWdBshWVLoJVP4R3md1p*J&7e*h5KOe;D0(G#F7(dK?)67WrNCn7H=$KAtYlrwWu zU@G`KXVpPbxkH1^ObUDH?)iZ87;^Jp?EUYr{8ImBy8|a2ny{4O?hnPF)bn2#JbHHN}YqET&N$pt@dzWYrF_rS_2ojU=XJ@XIs?>HC3uAmxt_2$Cm(fm{$4!bkTnt?6N8P958c_(PV!kUibZNC{-=cd&wPRQfZ=)x zslQ0Rvv4UY%)`V=}l1Xt&WmPw!H?F7xl&I{?_uO>^e!Q3tHa0b&S$;v(jTk z6?eD_Z)if5ODZErVgZxm{uAglkGydS!0I0z8=I}M++1E(s)dvMo8IzxvNQ_E;`SG6 z=iiGM`J4Bh&7fuyaH~Zje9hML6F)&Ssn%-Vb?JS`w`so}TKG zl5*x^-NO=TQeCd8ks^j#S?3^!utw;|W~wv+chD`xYL*JxIc2BP2TSVWGnEJ89CFlsBL*)j&EfHnF%_T`(A{-wN`>rhP=D5DW&9sk2LnWEnu7LE5)Oy0H03yQ@yR z${7^=O7R*R8UPp-KATSB~-g}9Oi2)R@r%#`jwMX}?*J>?m#>+sIzlQq< zf}GC@YxhUz$)h9{75=wNB+J8saKKMk^s&FoWiV*OEIUcViI$duA=^`PzR5LjM+V?# zZM_xcuJ<~6oVouooZa*P?y$gfhn)~3??vg*%|)^iqnO0D(apuOv7_|F3YGS{6%I$= zAk{mRy%Lz(n4a1>qot*l{7NF=er)99)7sSJ3E;}W!kWTB`_Hidco)1djP4&NyyU{y zXa&OA&!Wi18qo_xvoWHSRg*ffy!?d?UjrkTAAC7KB`qfhP@{H%-4&IX*9qpI=Sjjv zBK&8_asmHt!g0>z{zi+)!3FfG5zgWl_1$-H|3BQ(?`|dG7N8MGhdF)(@jsWTO2AzG z$6AN6kyM!lWZWJ-nx70t`}_Z=g3DbHRWlMaC?Ho&7Eb_-j$Y zxc>c)h!8(bpGccxxY0=ZAn6>_98{#iXNi8*eoUrXU*)#m+`S)~LOpXwdGgmXPcOh~ z|0(dFLI3u)Z^(c$kH*8fzFqz-%I51;wmXBSu6^xHSXx09IdSD)1N;(wFZwW99pxr4A2GkGM(%xO<%Guk((oN&{nn%HJ z9iak|ff7P@0|L~BjEuOFP6QIuwr-`giZV<}%9Q>2*{E3l)1D~Y&WZRw=I@#RK2gJB zapM%4J`1THCVUP~{ABF8dOXUGb0Wvptep~)po~83FJfu(D1HuX*e!(rpLzTJ(SgRr z3;S`~>eL(hswKQQ{8~rCgK9qEquw6mmybb|f>9RXYu^8B@rr$LOQ?Sd7HCT%vtzn$ z^%_D{GRS73-&oHvt)ii=|Y>o4YHLs_;JFRW}sM=7rgbIy48s@Va<)D}UOu`Dz#%AYi zPj|WKAWRI#A6R`S=y1YI7MST@pI$4LdkeowyF9JkaIZ4V=H_cONw2_qvT6VA5584t zq;M12-n8lLYd^nexO?~73x%OB@1wa-7pV_z+^N3tumGWwt)Lr3ZBz?QLR(o`kuuFi z($Vu6JU=j-bW?r;`>38baxg)FZka^hE2H@e5$`KB{b=YSdt5f@7Ovrd0%=G5 z?G6a&o=6S7IO|W>|Hr(CUm~Rs{qrZ#f7;09s?Sl|X$xS;PetA*ky_oY|H6#{1v?X5 zi+L~+%^S>)bKzHI(kdE!+hg>oIn5uF=xJd0b%gYz3#X4@L{fmDj(bu=SWQ>Ho*JLD zC~4#$_$WSo1n0`qB@Rz0@Q48xu2=>){PI7mgySvjA{7DYZJkl?AegpEn&g)z+y2FE zaIB6_CEbKk?QM+pC&RaKB0#I}V0Y9x*Wh(YhVBEGo=~{0JG2uOyS0rvQ=f2%fH%M5 z>QvA_?W_6G>T)x?W>7&3Z0-drzZa$sM?G1mylRQK<(&3(bl{WS(*K z7D6dneOvH4vlI{w&X3@aI*Rh&ZDIX6t#Mv|LsMo(t(w&qSD$7&@C(aNW|=xubyH+(>)Z zvizZJ5tG+bWyba=$~gsI+8_rFR}G=1YR9>`HOU^HlO_u;9KhvhW9e7{6N`(-q<^$p-qe zJgKR@(z1yrMfq zl0+ebl;81o;AvBx{^8hp|6Jzvj(UPX|dO2*I%ulbmJ4r6CUx+D^!8%<|4;GW8CAZc#+{ifRHr%w$u(Sq`m|+N~+aQjA zIn^;kS1=~95|HiT9trGmabx=~3_mH_jQPYmZKq|c6L;qM>9&b4UGv2Lc`#SgwbZ3I zTQ_r8U$PzlMIJ+|yU<@)z{zcL>zfMQ^JLT{o8sm@lY#4?-kbsc+_>oRb*n$oC#JXU z455p6t)??uNzDS)y^-G&uNK&=e_jw?&;+Y>;y3BXpA1k?-adD)pi`~Un*1{Gj%zYzT3cY@b?|=u{Aqe0=YDg~LJ&qj?=I?VzuBbFJm>jql{lDx z!G64t*>q1c{bwi^2 zif(?q7)aGllfEwVJ{yM}J0Vm~YpzJ}HYv$&sX^}Dcm}V_N|U~HG#2bh&gP0&bzu51 zj|o!p@!y4?&7kgj{?~^SWmx(3my0gy|Lf^$-^e7&vd|1eP z+oGWCVHfW++abTmPb3I^H|b~S{z3ahfuZLLx?EhW(iQq*{o!t6_$JObj*xY)qG_|m z?t;%%BE|?ugQJLw+HUFg>|n61vyOm0=Tdp4G>p^sZ9&*eI67)Y1F+y?O3P{nmwhF? zR>OzAxJas|qwD~IF|JgG`qDtplDZ4UgZ7JB)9}0Vx80UM&AZng=9e=-Lv?MZ2Elbf zM-Uqn7h|OnVj1ShY0(zg?sWAI;8$4qiYXr>(6fWO5X=nGVZSiisZ&VqL-^ja}2 z)ZXhS4WQMk4_`JEK|Ms^dtVuJ$=)y}CVah#-&cP*9{|s37?J?rwBs6f$;~INP+R_v z14W0oMYOj0b?~Xj=@)HUZLaufk3IH^(In#4pGsd`Za+_&>P2z4x>XsTJ6a1iOjfQG z57Z2}^5e;wV2A2k+>uW!e@4`~c5|v&ag=wu{jp>d<8HXK%l#c*NocNR100R;n!9Dx z9(l1BBw!{c35855M9E!|?Q9tIq@kc(gfTF5_%?T@<4@$^?!7(DZQU(ED4-sxQ^!nHd$C^(vd{#`?OK0`Xw ze7pa+btj|-*R_n_eWdGb&1jGtW$oeE%(6qoSq3ZoNNb#x#L7ff>8PEUbk=N|_@j30 z(GZ_bExT%kPOywMoxY5AMeO%vy!)8}R;IZ|hYVxCX3uf@FDsWHm1}VtlxRuQMel2s z9Bqx$YHeg!U<3ZYzJ^rmRc@ z^Mo1+o8X1%$pg|6FUGd_+b;@1ZYj--LB0&wyRr^LjcciC8T8%b>&A^hv5je2#1Pqyyj^3mmBSETWcAk@gDBne@-8T z8@GB2ov$zP9~SGv3K0)9IUhBY9X$74ja5`rlj*Ri)7;oBrIM@79OG|!xl&RP?;FT7 z@~!a~Ix%PArMvxmJ9^(eIh4Wk`(4@Wwpo;?S2%v5-RXv(J%eYo;TK-LYl1puSfiye znuad?0*ky*>ZW6}ED1Z5&8$9?eTA=bZwxOvvGE+K;UIWTfAu6}yYSQPQD3wkA;C&F zx;xo$J<_`$EA&L4+To7LMpRHSqh&Lb4P;{|M3`!brtZR`2?e4x=Px?#Q9^D)nOO(Y zNn(pOV+Qv>$Uc~UjL>h;TF}3zw(|NEWQ==XEp(B0Vq$WmKr+y5z@*o>f0b21|0|s} zD$M5pqw6c6qTIf>5kW*$q@|JW4(aah4(aYrX=&;1mhKKgI)-kBZUpIu??t`$dhhT1 zuQhAdJ2UT`_ryN??6c3apM7fDdAfJLMa^DnaPKMgrCPDDPtozGs(daUCF0M!CXHY6_)hTE|&IYDJ}8fWvKLDR90t z)#!$Jz9(yTJ#WEOe_$Oe3oA4p4Tza8l+N_DBk}7?EmMJG{bjVj^5GE>X|FiReFzlZ zk(c7Fz$5MJU&~;=n>ot&q9&P-L~Kl6v^k7Yy`Xj>b)R<^bm3_2tYmbUS=_&bsV(u2 zAgrp;XU&1jR@-X`6{#^i+cz?)(z#MP_e&XP|9&Z^s$CnSqD5_?$0El+p4E_NqIt(w ztbKb8y>NNyb5$!iI{`JBnX(oswq!UvNZM8@)BP}hv~qYnyl|sWSw~V+?H}K0BJJ3C+Pa~EBt$V&VSW|obDVZLTYdgPX+0dbq$=dn{Kzxv znP8^9`gE4cGi zC@00C5$8@~YmOWJ`mI8xG^07^G?P7>ywsjVDXl)1n$?@@#Mfy@id)x(RZk~Wy^|zf zq@{HqXJ0vOIYGC|KiFkU`?F`laCw|$;)W93-`q|e8gXGKo*oQJE0eR(qKm6sD1)&h ztnQW`B#*hSj>Rbu``K%-jBe?z)?tEH#tkj+?ys)u$`3aOJ-$QeO)oW_!R~5R72@7H z?JZ;?psF{2dmdYfB6= z%WU3JMB(lr6|5h65bg>Dc=l#h^{>SC5V}~g$~V^E%-^vNz;8y)tNQwsMf|vQ)qb5D zTTWE*IW*SZqHD2}(=ko?z}qGshC1%_)3R;EDY3TvOowvxk9DS)O#^2i9zEiWFR{ZQ z4JWeIA*3r;*(2uVld4*?Ck6p+F1I@lg<=T0QaV=M>4}3a{&md|tj=PXuf)mR7a=3! z<6Xj^ry^3uq)4|N+*Rp$iC{GS&|gX~-XQW%_e`!y$A#uVDX~Kn=ek>;_=FopOv%c$ zQtoZC(6g}Gm5|&PSJ4r5p_K2{xWczjvZZZQJS6hKtF=FN6jR`jMtp0Xd5DJo@K!}d z{b5?lkn5PA_r;k2U!$;j^YH8dhg#ow&^r1!;^90-Zx28%BdTG?*=K>7qL$pig=ytn@Yg5D!zjo$< zZwTjyl<(C9M7uAuCSL2sT6_{seogk7g=d>~2*2x@Q$)=zjmSZj{C+v!eSbaj%nGb( zut9Mrt$4@HK?SElVh#V2)V_P8qWNdDUfG#N)PXgMy7DHgT|A>>V*R!Dsdvyu4C@@M zVhs#HUZuJTHH30MJAKWJWFTMAP>yNdjz+DEPva2M#`+;Y?AS5}L7l?Z?pqsQcIDD>nXyx8JNdl0vfI7dEAo( z`lh1!vW;tTc8BCFgH*{7p_t1{l=mTZd zWu(NwQR9hFBPZIjpmN0mb|K=)iRO)~{jD!mp4;1APy+1%!@WXn7aIdrn&rO9Wd9+n zc%D3sr*sYRhxyOepS_@naVDZq%9T7FZ)}aXq@M3)$0DBRb6Ag|M{LH3FQDjg(LD(* zT*wEl*WKi>%!gSC1^ToxaOph#=?1rseXh|z$nJO^HA~rY)b@(oit}Hs}{lCBrI?rII|O$gmd>34cgUYVF@n7`g$F`B z#G&bJC%;%VG0_}Vt{rWghf?#Jc8QyJI`O=ADoVld^9g;%lm0Vv@iz4SYIFo4ZcI68 z<%w__jZZ99X6!1p{`%gn_9%T6<;5%$g*z9K`}rMeCalUqQLEBb1?MFtrcPC#@Zzk_ zGE^A9yiK)WgaSNvGiTNl^Xd%X84r1ivcG%*`wLECLJXB-;g)x8%+rkH=a=iDQ8jGu z9`5RC5tGQ=Y4P_jqT|Zkr#;+>S*NRayp@7yV|#!#;`6flFsNnmHau^4X+bW4Iq|!A ztp;8HtoHeeS@L*W3x&J2I?b`QxZ9!C!3!^Y#T}}BA~(CzFXn=&d=TDq?_}7=s^XO# zBpFIdgg9d6|Z%v?$xBz1s3}!E$6q4#I~+>nS@MR zk?CR&@NhKN$EtFtV0cmYoob@i)52(!55JH%z&S(Im@U`!hk6 z)$fEh9+0yU_AKr`HnYA^sik^%C)1G&ro&#xY9cLxtb1|W*T|P0qfGqXA-tGrpDs^d z#ZyHMZbP|AD>r}sCFR9CeK;W$0;;`uibu5@LvGKWDY#=9jD77$jz>9#pRc{aP1{RE zmv7b%INUSTAWM?ZJ~<9-XEeMmX%MjqEu|%NCY>Y)w~GFtLc9&O*Kz?L%z7pb8de-g zWJFbF$~S3I+RawBxKH!kM8#3*P3|w->^+Qw`m^V)SH89e2Z7D2315(Aeh7y)ZcVrz z461VNiZLKIDF37**GSdl{KUHvk2Wv+uko~a7&Nw+gdFrDDt61rucOsb+N%R?Z8UDkFmht0W?wJr%P-1T^xp+_Y)rj(f_GZ- zs`v)p?;rO{r|PGqe>mbX+ZhT6JU(I+wCR0vajO-|EV=wnt+pLMMtC0mXU(UjpQ0J& zXV*79B5G^CkB%=0^9$S{&cUM&bRvG*u52nNlV6`MgN z`G32nGo&R^o3lR`1m`AgM)u8^J>Wvmv-Mv#Bt*=zT$;IY=e8}42)%M-kL!~Pz$8D?VYwSZeRN^O&? zr$K@DSx_4aVBOCr>{bdpJ##R-tRdXx1SK76i@^(|`Szhx*HhEvj^KiOtl5-res%8= z+tSu?`}<~EZlAZryqX9cYiWomTgXZ|T0W=%vwKyS!fc_$uDtQKHMam$k_WL#Ak(<2l;($UXtC)unHZz5UX+2FJ2_0JcCaHbDxS8r@4sJM~2c@fu zawq8~(7A2&su=Nak~mE100XnJaPpI6;dvQ}MB$L62VOIdkshC~<3AHMIokl-@&>T89&>3iO!2LN%d%z5zCK;cc837U__Qz_*GA0_K zrDS#S2HOu6^(eAf9eK^9c4O|P)Vql=UJ8<03AaMu%Bqv`Jvjm}2g3RF(bm-XP5e-O zCOS&wlSHanbu;qYEY4YMeg}fAt7&D-?sML`nKyf0R-f&xBF;?Ea&AneOKQZmtXjLx zwmN0C4+l&hz@itbN621Iv`&Wv7BB)whoF&ESOmW4N1b~TOyD98zn+JtcD|%~YQ?Pi z9$Co5f71ZQ){7&h2n)X!A;Bf5K!B*eNaE(TI!kML>?{!?i_wE>?`<}pd|sWJtI@aq z!taiu6%@6E3`^`^Xk;6tz1EXzoP;P-?T5yO)s$2&cZ9CpU#N*E68QGO8{aFu-Dw7F z28ko|sm!ayLaKQ1tZk3)aoXF!acuHs*egS!xr-VqW$?6aV=n7P?(C%!Mz;j}eWOex z?NtQdSlObwb6aH>USGD zs{v5ZqLJ@LQzp|6vbJL?@gdvLO?I&jSL?_8!oXYjw^AX7lm%OuxgP3XwGS;E(G|b!V;uFfpn5HL?u0VonpNFqDJq+gulFePV z>D3bBIEK>Qo%Vkm@NyJ#j3Y%Af%gUJ_%)!PAbWrG|JWbmAF7{fR1rnJFT=rJT}H^l zkEW1wOeJ@Pa=fgaVOcn3U-vzTvgy*C}^X>zJ<(L=SrIn8|tdS9hu z`OpM*H!eCHx5G`JkYnEFuvR0f%D2s0Tt2;Y9A@r6kq=HPI}%1(s-l*LJ~`P9u+*KsGPJXT8!e?5cOn(LyETa|?2rE9dnQF9vu+17khhi|h;V*^J|0W{>RNFLyDbm;(;cNSK0fC9ymY~=&)}IOFMYYioRPS9*%jqgP#{D8M*ANy9ydQS95dEo_h zVlEWBHw`dB;s)K~dw0q8az3jK=7r(h1>Dv>5&cwc8Is9pTqTSAfsOJ^v|Eg-E2gIz zh5GQA(MxInrHc$kOIHTS{bUGG756F3#s zO4V+NL7zUZ_Mj$(J?-$)rS~5GrERqRJq+P@G~N@5pT>VE&mN{q935x0%eUqKz3jtG_NZmp8_7LJ?D39?qIno{agh-zsfHxMlX(j1V4}u8 zW~X^xE_&s;?9s^WvioEpfgYyKRl#}7$ABUDyV27$Zj2w$uyqRVba4mztzMCG9E2Je zNB49a*VN6f?oeeiQy($Wx$ai%Pn3HfWLLT_KVHx@TQ;nuab4Cpt1q^;Z4%q+b@yLW z4jI{&X>s19=?dBK^*faHSuVZ5Aj6*PNDso95LJJmy7EWbv|Zcd;v5q1%V5O9l=#9z zk}sF0sd|5~e1H1erk}|9^3iVJS9gRu4WC~z78&mlE_J$R zriPwk%0(n;XwxIsv8v=RKm^R{ux}zK11_1Ccu<|?!ioY)^5~H*5K=M%+KZ3B83P^F&j=qyEYYHq2Uk;p#HV;Vrrz+yV*JLUSJn--kFI#t2 zbV|%Nmu3ZeimI|^Zosb*5^n`RoX;f`Q>~p+1{ED=d5Y8VCHf zN!U*~`D10sO1%pe$57UU)rZx5_Nvymttz5N^Ox$DRC)CObXBy7x~ z|5c8(EenQu&pKEU_lRlQ098_1yGwWUgW8U+UD~99{TWxakh=miRb2VP zUoYLn<@F$=dgPDTq>Q*!zgabo-@^1G;4ZS&0z&&RJAwex_yxZI2k)rjbEmTD}+X9TXA=h_kaf z9jIq)UZVd4Y5LV@(`cRxCuYxj-K@I<%$gnSR^V$>v1J~yzNZv4ukUuT6k`aPw?nGn z@9iV$cgm$Yxwk?@&JOBm>5PZzUc@Av4|Hjw1@C3ap1}#57{BIB77#trZSKU^_u(NR z0^o;{Y47bZ|Mk$_KnS(pjLW~FdjszvcCKs-GG9MJQ8YLqjsK`629dXW%M9s_=6ve2 zz&YXk|6N^1_pXx<9#k-nHFX+{zZ|VLHDLU6yD$J ze2Xz4QgN(g7m+3Gb?ewNi?;C>#tyUWhb>wI5e_6#Vj{-tFBB;>145RH(J-Y6!ZUmA$3b>RMm z(0mmL;rjFEUljx1UnKwyc;;?9!UQZ_-GUG;(7!tN-|vs~<$Ol4Jg$y$$oqdw{NEQC zI8RRh4!6qieq{RRk$&0u7t?=OG=%HF*-C#sJ_RDjNGfxOi;n(xAN&iO>76GHLGUwq z!ZQ&-2%mD0Z6p2xqvBa3{s{gXfA+CmktBmf)WsFy3Dj=-$5CU2O)r<7tMx%g;1Ky| z*JY3)w5$AG<9{yNC!b>_jZP`&+m?Jhnj(Kf6K3zS0#JI`Q*u%?eEPNZ{O4`}WVRgq zJcItPihMLfB5m89tufI#{oSP__OLqoe|PUc7cvo#@L~e4Do!A{a~#L zs20tO1-rKk<`~^2>vb z1mJmlKCa;W@z{2B$nt+pV4&e@kAh!v5;Xde#{%Ia?!TM)dz$~01#b)_5izlAAm)Rt zw3aw$%M{@&FdqMY{aZtLKa*VUjU;#shWhFYLzZVh>EVB>!Gs(0D-@)j{z)kIe@l2Q zu$?cXpuiYl9f*6{O)AM++Zh}rM98VU&tdzY$Nl=4gz+R4=4Uw>9}q9Ew$j6dvP-OD z^04MVbV2281xtMXD2EP!tJ^;S_b7W+N*MnyuXO=s;g9_Bn>^oHb-*GN~kIF~vCxrh9a=Sllu~y5R zaouD9CqUCEcY~#lF;?{o8u8bthXE?$#lMusZ&{L|fdaUqqobqc1?3gQm{DGt^1GI9 zqx?Rb{e9D~7D7NuhCy`y)n+C-`eU4e)6?=j7=dBa2V zPX_=AM~EN6p(o85%8$j2sK90{{U61DL?QqPD4+-vere=?z4P!Be{B5?SLNw^#LKUw zBK@HUE0A~|{(S0B!PK+%1(1q31kP+H20k;m*% zMq~iD-N?lA?YfdVV?zW2tu!WlpNgku%uQpBTX~N3bU6QrKs_I1!Rx=~-7=*&K-8@C zK~0qm=*NsqU~$a|LKv~qXo`$H<{)G?6v3KK^HNFG$6Vb^tT@IQF<47wHb2Fc+?^zI-N)~uJ#2Sk=(%W#W8a@+I2i4MsYE$>VRPVm|F$ zB`sQr+|3EQ>`9wXRONJK_dO(ZZCSLQiS8|>afsRAES4O8a)BukjAw3TH-LKOw|kD^ zd||d1UEHKQpeS^j3@7b9P3b8DJKGoMX)<^ ze~Al~gzqg*5qI}h#d%>wD%1gk4L_wqPh1(ND!kg|u)-;{D&QSm8V9%GCjZ@Kk-YTX z^@CJu8^coZ+bs(>$IHuyQc+WGznf;V_g4{hJrPNE?Zq*3U7JtK!WwUC6k+T&*Lvwo zo2)u^>~r6HoeUy4fmEY0*!`S}wSQcN9ZF$YmQhw^P1@mTlv8Na9kMy%_wBe8P2`8x!lJw|5)p89my6VZ{!c^$Pd3rV@W(ZmXAuEdSe?vQ08=;P z148dibCSU9Cj@>y=QiEIaJ)=rlkbO9MBSrno3|HCL=qOsDLh%G`9@okCIc3!Zk7w5 z8Sm#8k6?S0q=H7Q?hnU{=ylUnnk@P>4o=o;;0}%q(8bZp>ZtK*uOz^2>Gq3`5j2rh zfZw#IcB}EIBdf$NO*0zd?lOrxuDO%_{3yvE$z7$plF00|S;f;5oL~<+))Wr!df-TW zhQtsrk{kRaE=xhj4{_bglhfYZ4 zx%(XoRPW+dlG=lr+H~2Zs);^d^F#d@z0HV3_1lnQFqhWVbd(KmjQ8)wG#P3c?qBLY z=_Q~MB{vmx@qo#xh$NzR=UdDHAEdai8(Uv{#*6v5e{dWy9)?Fo_${55v9h|Oq6Lk9MT_jc9Cj)>cA7e_ljHi=I0&fvL*=dHH{O0dEef^sCtUUE&EF)n zxen7*(oa)Ew6Vs^+&~pp7BvUdx+USt|1_`w1=9L&YXJbY^SD|f<|Ft5?JfxC={Pj2 zAK6DjM!@+1$E%?+tlkoLNbrumcm8}ggn~h3aPXmpVMKY@25Too-=SXP13HQdHQXVy z{>+=35sEJzr30Z?8tg9M0Nygy%hxa>r&^Z|bruGP`U= zCb_^~#?+!U;B?~Fz;LTk?Y5-`g{zzzQyqT`I^u!6w!d+N&bcfa{~33i^~U3 zMqjmlZo4GLmEjh*$VKqgq4_3744LRVuleAxzEd@qA6SSip%o|dp4#=^jqmld))o`o zUJ-gHeZSr=37a+06m;AZvUR^7bBar;McJ07)^zzCPCuLyph zGD7S*vgfUhTA9}KVWiIj6^L=Ye=Ps;_#eMM+AuuXB!D1~rt`f7?C}g&NH=9_AY#Et zU*n>(HZ>spImvgIoqmf@GIb6HO^KPEuCi9fJp-cRWp-ZSGfYI$;FE5nXAy}<$Af*H zhpL^r18n48$c%d$Iu$!yQId-&nclFeu>DtUS zBEB;Mqj%%qoaJdjhb1#btM#dz{E+0&AI!NQtj{(oD;$W@k4@QCM6|!Nd?)+fbJnRT z0pxeYFQzOqKfec{Q0<+t9jv+~IoIk>5~GFB+$|Uq?Ng5&HQwmK+6kXFuF0C7CX%sS z;4m2}qoTx`d*tOuvF>dM_izdKGh>dGVWQwYE`%S=-n5hc$C5M^0`|m1&R>B&UB-tu z1*$ZTC7P+A1ds(mQ(PGuyS)O`Mu}?8ZHUp56p;UoYb64O!mHXpbw^=P*XZ_e?I_h< z)$_JnasKovx7x8aGN^R5(31UXo_lj~oJl0P+U$F9EcbV@h+wy!+=}K3+nTx8pF*gE zv_a|`NT9~>q#EX&7Ci-W~aR<`;<()qoF`o%kA-dx!eI4Qnvz3`VY(pC18#?VBgH+lZJSBXE=*z*j_Y+^uaWYf9q zfn8@i8DxtV7(3nCd^!Xr#;98t=f*d9mAn4tD{jP*0AGW%C3miW`8%4s-qdfnGcEfK z;2e>SrK+d-?Q?v*)%;M%YQ#Rc*8Sk}Alz@Rx85nvYRN%>dA`MC zb{?tmT7{EV^IN&<0Q10ixd#>+udTFFu1fNuXTvpzA4?#m3*6cWSsM2YT6bVTE1*w( z-111=lHuzxw}t25**4ag*k9Xn!@@X~sL^vJ8I6zU->kb7IUdH#?k`n19@+8ZU#^;H zP6x)#%yWC#y+J4V+fb@Ue)1CapT#c)m@%_1O68AE6X4}dL6mWPBpVnwtz?YhnV{B? zZJTQ@i~G<0zxyzQ7l9NAsEwM)xSsyR_D_<-8mML*4@UiIYA82IpD(v{k*0JVe{8l( ziJw&e^6D&OJ-MB+3r8ycb5MXi*6DxQ$ao(;LwM3~Y|xNl@ermF-GSYWsNvti1FfHa_h*t-9YvR$5oqILam^9$^N9^AvC!+`MK zeV*}u#T^4J0UTvOlWQ2lkN&g8w1#y4^P34WVCWoz(T0C%tp6jkAdrMppuOUBAx7v^ z#ozM$=f!*CkAGz*Ens-8;!CPOJWf24upu$f*TDSB zamSM(1+=9Uso44tpTuJ?|DBTb_b_IVJ&qpoFn;&X?sX7_X#GEm^;W}um$RiN{37tr z(fWODoeL%N*Sy1E2PzqM_cH6@?;Fo}AuRrE`mgJWPk?6@W5vgY|J_H=|NR;G=2xup zM*yP?$Shqt=jp$FS-a5?bN}A2=RXQ92FlCFHe_TX@Ob?rt$zr25|?*38y`@~v%$>& zk6%RD7h>+Wh5?+F!GO0QpZ4oN-rr0_p0@uj76Tt>^{b;|#!arw!@nE;crhXan{usi zz2>@8=Qx)3pcjx5FgNgQu?#V~`a!qvYfRL>uAT1zDH}jFAaemV@-v`W;qg?W=9A9P zL8+1EaX1uxQFDlf!`mLt10+gWlzkNiBu!x|cT^l-gx)M03j=A5EO}}xUYsDaCb@v>X=OHdgI zt783@=`iiog(!16->CF~kZ!$Ztpv>FvIO6!{k<=fOpPG$A8H?K1VG9)*9G(vfNnVx zg9y`n=bfXmcI?|=S7*@L!en<)TYT}*e{Jn{-rQEj4URnN3r_*v_oYgIFgox_Z3wv^ z%1(gXkh#xC^-a-UgOh`kP@W0vR_A1Ack2p90X#HrK$1gAVO~XM%Y}N|jedY7G-=8K z;l@7ubOgwVWE9LLN;aEd@m^b(KCx>}Fwa3X0G>K-!Kv~`*9lRBVV|J!gnhzn*Cu#L zTTMRWo~h=?HPKdOu*QYQE@5La{>iDf99wJrvH6bumU5)L zq97VeDyfU5h0y-OID?XQKR{)Mt~8bkFC9ImB!&^R!$GvfEI}ojM`$u_A(Kv_m$#0F z-lf`RbdL&JfCjJ~0Wlm^O=`JoyczGa+nZNqz@2S5)y{X{wylOA9GiIx{b$~Jr{EvO z`x0q?C%ygw>R@3(c5iJlp3IE8LklHd2(asgOZ+KuD$g_06@jHlg9r!tm*$#a_ZEEE zv`=(0oTG^cgOix7*yEGA??{~PhZjIj8y*$xR-padqv26!uU+?1M|v?SyNi>H?dFVYf$9rrCr+4ZU72!X>NmMYSMM*a<|}PJxM%3^i9YQc>Y+}&=dw^~ zX!FzGMpN1t{ouR*9jf#dwGmzbKw6S}(vvUnd(F%}}=KusXtkW)rweKPVZ(lrg#)q!0 zt_A=($Y67UWOe`>J@Hn5CPmN93kh5;Qr)O;&*>v_O~>tqQHkReGWNf`>3N8;@0%CS zMvdV)lnKL=Ny${|qLQ{%Bo7Cwga$*&QFoJ)8Jm972-AUkczF1AO!ZcTm>5p2JIknX)y<7tM6^I(+e{7Y?u5Hj*;-)Gr00ot@1lEuJF3ZcJ?}!7HwuvA!*;Rh_=Z)p zI<&~BsB){_m{tH0zB!O5o#KX8%-rdndg4&jOOE2+%|K$rn}H;MKfh2+mQv|-uJtb& z8STc5a#_Ve1K0>T@zNVX#8reaqxy1)wi-#)@!ICJEuO?n8#+L+7MxcO)`WABqC^QI zCGQ(WpBz-eU}^MM@MYpQ3S}niljg`--wrg>QqoAyEPw>*YR*LvuNd+w{167+!%YbD zP!rXRjtU&NSYu2$NWSCty-c)#Wb%@NI~*TlU{Y?pxY4&YsIVVG8%vFlG;C;NV6us( zES3Z7&laili8{JsAicPp0-1{9Kc$nOV+cmUvXfLe&(32vv$7y(i?p;J!2XEP$J!Il((;?tlZgz!an-vN`(;?BG^$MYr z7om3^{npIm5*Ic^jVOFH=M@4@v_VcLw~K9ETa|<79FdLz?a|`n1V)6zOEG()bmqUT zqTR=Y->%QXH-V3()K99xBd{az3{g=vIW%SpYA<4BiX4y`$+6+xuVR~+Y^Pl*kR3CY;EM!Kr8w>)PfoAs3&#ii-@mT|T1Othmi zW7`~$7BdelAsGxRk+Ku6LIqhTY2%Jy2Q-H&y>xfd`x8S=5_H~ozFrs(RRyTbYLBPC zvsyWC)+rPzqq(EZ9zB#dpAAY@%E&R4@+N~StD;rht>Wr}?Z$ggNtGCQ2X%6|nr??N zAMOZfM);=})QxVOW4XN)*=kI!7yE^fT zZe@?Dymp?;V{oZrZoE!TvfseHKtm@Af*D1`tmcyy2Cb?{`(jps^6e``uQzlmpeeLc z2tG$z-5=n3#yed{RfFz7UhiWusd#@Jc+<{<3Hd~ke;exh=V}EkSU`gB5GHbO$Q>$6 z^464DuFZk63CJ$C*f-d#%=M{F@p1E?ThzKHAfb7wo}bR z%OI5`bnT!qVj4<5-X(R?YZ!PtLqkdFn@dn2N)nIiqe>EshIhT7+i05_c9P9+5Bh*} zp6ivNBZWuBoNYsZ3uRUi&esh`Yv%Y`IFB&nt*zyosRP>B} z7=>y+eyeEm)>+%GL`Wwq4ZAH2U5YkVAJ#MyeHevCZ#_E}Fsf&kupV{6C^=9Iykgt^FyoX;;J=}*@ z)a1oeTg%G6^2;1sFvCP?-dP$*ZV7*mxGqzr^7WWdIEsA8^{8QrKz#notN=hScN|vl zQ+a=%d;-1zF&#^Cm%GZOo;V_9j997cIA0=!TBZdmsmyi*u>Rc2@TH_oH}!K9JZwnR4$rwik3$9+*v39UR1GNG6LDz@acP zjNiP9dKsYGbi1fp`sI$U_E7h0Q9dC{YH~AJ)gXjZQ1AX=Z8HV32JcIvYL{=pG-hDi zF=z)lUTJq}Hfbl{E}64hwY)A#I&WP$@tyZ5nq!n0Q>d!XxA-Hu21JF>!o&=}RcFU* z$uA<4+Bk>;rQn$q-oVngV`vCS_yps6r^5zBhB(1Pp)g1knRr{p zejg-pZfP1TL^KjuiT9kw(z+q{f=I|@_}=_dCk!4;oQcST&mV}&efIBK2- zp1bI6@3mkFkWYf?&(YZw)X$iR5r`b)=)E$j49`YxP;h_Oq<`H8jREhvMjsYjSi)<@ zyg1WsF_BuKMujARq(|%><7ya3C@*ArG@|NdwAr+sG#=wVe*R6DCT#ayN-M4~>WV2E zZTAq+2u#KO(G ze1aI>;k{@l!p+yff4?bCJ9V6!i_G@mfQiKLGR5d^e_qx;#AZ9#cO3 z1;)teLz02j5Z<({O7i^nWnn$Rj{=90yhO;s!_RVa6pg6wWA=BGabnhDjTlduh6y8*h9nTFa$}TyqRMi1 zNN7;xB?6~zn-S%d$);=g(!d-xb^+bf!r4l)G6Qr(BEGHEGy7=k-!a!&bysUJYI5QP zTF+Ip3dz4DT=ALS|4_cC9!uWyReox=Hht+%!!i_;c5i9AHz|(Uu|3b$5<)ihf|5s` z7|K*7BMDk&#F_yGNg=c$kh~M0WW9e}^2DxL{Qko3;av)BraT`e_TI7VWeg+l?k?vb8*Dc5f_f{ys=QVliMEZ#eDXa zgG5XYQlWnzE0L?OGk!_JgprrD))sC*`M}n(kESX$-x#4- z*{#rA%Pgs=kZff{7Y~btwwKVMZZ{Nnn5>QviZrcjJ?vaEq@Y*W(ET1Kv|gK-o0qtj zWyKj%;NY7t1Q`V&Z?Nh08{#wIB~3)?$;mM8{qy%vyjbM;=_e&i+^@Ew(g_uTIXAtT+C z_@yExC@jLX#^d2g4>%w8UddmD-*0A6@aqp!(zW`{>gTCM#b=FTWi$G#^KsyYzj%wX zDQWVepPorJhSUO1ZPRw{i3zn+X;XU=^oaTlmoe0ol>D28u_aQv7FBc9m3c3F#%PHs z{j3C0AxOy}4-$II^pgmQxf(<8^%^u=jkQ^*(N4MgpyBNLU4@UbZrfLHXms z5?R1jzJYpdS~`E~Ii#ZUyl(flj7})ABf7Ta#d47*bdc(^>_n?Qb6jyQ(~H*osHE2T zk^Dfy%Yv2wXqQ)Tc~IPOgpDUFqtn`_vh$_}sEMAO8t-?SPA|n7S(B+N=WtZ*;l6L8 zb)cMGal=vbdX!qU|A2w45rHVx_{8JTCZPEoI^dV@)A|gyK#>m)c<%EH*hP2nThz

&Zuf0}+QFJ!ofPlf9*lSC_UigiDo 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

rzVPi!-9=>uy@%~<_ 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¥pzq8-jE3m5qbk+Pm8xi8xrzO=kt$tTt7VEVEyE$Ht*28rk zXNwc9`p!ceG3^Gfm&Qf7__rp3&*gUy6C40LsgkyO+@LhwTjuyZq0KbfnY&10<3QZmOEt(BguB_J1^!f_c{t3-b^d#)k#{H>jB@yX)RgK9cyM*uc+Y) zK3lTzvto0~if3!Q-L7xGG19j4)Tz|@UwIbot?LWbqPO&Y9hc@}G!ue8mC^*78}aKE zbh)hm_d={_BE94U``8rN2u!d6EHf!z9d^E*yybM6Q8a9Tt&SVqvSH;~f-PTwN}JvtPTP-dSAcGl&oz>~)-BlXlr)*4+n%TdO0{EFW^gM}VG!SHV7@J#r!Q zR_?9`q91S}G94ZqCeiSr*(;n{F{Lx+S%G z@>xi*wd?KDQ(pA#)FnwNjvHaZBcJOxd2Wi=M7)Lo; zuDNc_M;moU4ypr#bj)=!YZSEhfK8alg5GE0W-gm6vsU#H!0Tv2a`*Z5eh+z7Nt(bv zfB&#*PyEUnY^0M&G;NHkF1M0=*agCwjRm*eN)lsnd2Kq9ryee7X={s*C&reS_rHF_ zW;<&4+VZf~y%V;*W_(O=R!Z)!XZV$=9*<)*82k=!Vx!UKCH`So%iz1UwKRN`)bYb6I*>!z!;y$^HhX{CmIfi1P|Y%#xDaCB~y&2D8b zxWWl|Mw95*dnCI|ge%g9jqN_noa67&DD31HBX2)(qE3#`KcuV!F-s{ts8*To34WYVo07pDJ(- zv;!X+s}puzUSYt#%g3&K_u5=v_PO#7PE0Xz6a!@jrFkA=_I}fa^XcHr;!-i1Sur3^ z!mjtf87%7q-(jMkVGURgQW5VJxnBLHWai!4?C_&k$=Eb|5bPXH@Z0+P#$%wzLDlPSjo2Q$TigfO3~YX| z$G(vlSf3bA$%t~kedN+n$L_~I7uU4(+>hVGj2@D;jA{4YjtZ;hUl|${b}YT{UEXGh3$e-olm1uv!+8Q=UY4@5~-CP=cGrU1zs1!7}C7e?uFO%=X?XaImyn35D zRorDc0x}jjInFnU-`Xz!G(9BS59^}?Bkwh%sjj}>f#=eCBExfh;o&jrx%GqNPZe=_ zy!*{cHTYgA3KoP)jdGq}Y^V;H=^^*Sxh;VVj)1kQuuGsc1!N*h>g zH{E~i3_VAa5W4QutEo8!QMiZ-s)bY`xJp*sb#N=cu-$ZwSg$XrYlKz~+>(;5PkHeQ zmtdn6xOb!B2{2^nk0&hlw>-s*EMt~i5@|gFXvzr-ialaS(Wl9P;pyy0F z2pC1es7-I(YC%`iO^jca&YvR|3r;I{-|a$!4;4w+KKya}7AL%FbP%-}zTDw{9lG}o zlV++O>OAgVq1rRIMQ)igK(M0+7|v}!q(&ohP+NYnliwSf?g_aOF+hcY#)KmHJyYu= z=RaJ-^pLxc^#^O0URai0{?RS2g=Yl%=RvJ=t}fHq@LC4mCum^e-nZSZdk98pqDCKl zE0uRHJAZ?kPmY}OB{Dwd#B!M?)Wr(xH1Bb=Ql#X_Rz=Z8^a+;4)YLN{Ml{70TTP7+ zpOn&i~XkOVvHLHmA7RYR~^^n2StpBUcq9DSBSuf9mS=0xZ={p z!@2+VvY*k)Gz`9D+hJKUb^7$_Vepz;As*}bb@fw1k2`_ME)==< zzEAxA{d<+7VVzSO)?G}Ew8ir;R6~-)w|nzBeD{*w!Nro2RDmi+{CrNmlXaZz#!_iT z?78t5l(53m*3x6gABEz6J$5-PfL_-ddvzfE5Jn|R6~)pAY{yf;?(l;c)s2>Vc! zN$o^U%XD&lgGI2{A)Cpeuz+#b_@m)dImuX%U8^N@o2#{3bNGs(pLnJ^7hafed=*OZ zK9n{(=X(5;{_n1lYZbvGb=cCn3TPhXCB!bG#Dtw~vYY4!$!gW>pZX4uuX79cdq3oO z_$_w~e25wK%&tE9@ZG!_r9&>uE#}QBKiS(OXXSFjQWP+MKx@eZhrypHajVC}(-ffz z%%;q?RW=Q0if39wS(F?%Ki^_GkCG8yzUZ>nVb_p&p?vsQUGQ|ovDKR|rXw7dw3{1U zQqnDZ*ZMGn(Owcv-x~|l)}H0J9^}&8n3>E;PKjKs2o_==8cOL>dnz=T&y(^xeSF+K zwDqI>(ely!^b~our~cez7Tfk2M0}Do3mwl^Foy-ogX#wI-W3?x9+mpL+Stg`BXqa2 zxH`iy`pLt&`fM^ugC&PQwb#{L+aDrZHGgumoj3ax+APFCdvrzSRb1m9pBJyKdJ^J4 zHTiL_w;63-<>*6D_@w@Eyj(wqostRdpb^%r+gj;xh!#z_({jhAMfVPikG|b_re!FP z>4IbNyhm)iSF^704Ev&`-W`eh?)lQybvAu<)7jCy{EXtQ|Mpus_dfXje8xD|uzA>Y z$@_tGLrvY3-9~uF#&Y?}`3E%R%SNqD9ygZ7E!s-*T+2ES2565zXf50#u`iHrIM9za zXY}P#F{H#AXDCMa9(MWL|5>;|A$4o*SD+;KzEoQ0GTV-~yMfVkmZL`og0mW=%~WReJieQQNlf;xjjb zlaul0?*Q*5GGV;=|9a+}(Tex&g!(EYlZ%pRt`$>Ac$qoSsYX~ms(LyM?`jI2j`6f?Xtybj&olqtFwBvjBsX>+q)F09=5zD9$M0e8TL zQXXYG*M1h)rH*zsZepIz8&j`%3r?*tNOCXx zp50RUvKDh?zPavI^Ru(TPIso~zr8nY?Y7ooYnMzOws;(EuqXg5E%hnGGr;iXYGJw( zIot0z4ULP>Scj?p9OxH;wms3J3~!Y?UrKV{D!TvaFDj)XMAYU@zgV9T7Xl?Yoheh# zpO}%utk2r++%QhA z;9ZspJ&N9-vZ*p?|Jr6PBl8t>wy~ViI$+UTrfc$jI*aF^#)Xq2tZ>b6n5aRMW?zJ{P>Y9$~o_%Hp&&a>JlxGP6&!$NEU5g4D2wltK7~ zyAK#;$;TPZ=0Cj^%5#X8>AYKD*%Ji(-{Zk=icuHEuRJW1G@;cJ7skw+N{=m68)(er zetSZ%Ui;-E_^PfzLH84tqIb1g*1z0z9UG?XoLVEJ`f6<&x1#d6!W+ADim!Z~&A5~A z5}i^Wl(gpcFsC=I*mQoyG^0jjLee5~skT(R=Wh>Be(LyXJ4|==wMc>Ki}h(I!PX-j zT7W-f1eB8*CQBCol7-f>^eKaPjZ5>+PoUZhN;-$qGB?x|-~M@l@>U z`N(}6(=Yqz*ytDRqBN4HS|UyCXOWl@Kb$P&95u-TGfTH(JmmCk0XW!{l$sOKKi z`IeqD=1KmeB!!uj>vV}*lDp4qW>=`9#xv^%XU+;|YAQN3NdJgiTdEsse5Y||RCGKs z%s*vGJ7vu-OQsNo^nW)&&zU039=wN^)pui0L2`w{k&#dZ1rD)jJ+J#O|0ZWh!gh)B zKvk?;I*8 zPD526i40L4`!1##{Y#k>OI~fa2~oZo583DKz{Kn-dd@wS>(Tr7>S8Y3t2*C7Oqd*c z$$6|{jOo1HGpY45?b-^h#b9I(i!l({rYB_lJ44YbiWCBF(tK4D@Fxd;r}M)MBkr z#!RHHNrEg50tq7v(j~=vs@wU$7DXN04AEo>E>2yyGK6AJUTHEul1QFI9m-nDmK#zZ zt_?vw1;%{J6N?-&v{CKQ#Kehwb348=dv~U!hQ#mYASg)LI@WSasvvDvwsA^d*6w= z$Wt%|NDpW;S|Q;$^bbazN_$emcu#}P=$KC009hZm_ayY<2v1@e^<87q;ebaXOznFa z1Zu~~kE}Lynp&0R{f=@xi69?msB&5}KNb~I3^&3A7Pz(Iw%pMN=|R4kZ`PLDu|Ca5 z&!HS$Iq-@`eyG|q3?jvsove%~<$+m9PQN^%pe!3eY@AZ%b5anI7)7v@&cNHIC@u{9 z4zxfc#wLQR;)-9Mf8Q0FZQQ{+&>J*|AccBL+2WnbKm8-NSwk1>?&d0B?sXVX*hig$ zkDM~9RT|>4L88eY+-MU56_KBRg5{xmIhwQXqoYb*1Hnehk)L@%xS7E}liZ^8!S$#;<8ew8|++Bnl9`K%)gQOZ61kNUuTV@xl+H zgJ{fP4x7aQac20ShL0y9H6>wdZY;PcIlvl*bfD7W-^sq4q2L-DYcfgi8SaLg6>dxi z&GNv-6so60xrOj^q8W{b*6XecI#qZJzQ(~k`N(HDOqdm&4H_xKq46Lev)(sTfr&wMgiM1P-(;3>^m@c6=gi@Hz>Pa?l}wFmZ5Z4+DYQ27H|2j4$pN zNV|;q1yUv8E`(e)q$E59+8?4PG)+ha{D(2x?J%if!=wT+s(H3vYd+gXuuVkKk2Anj z@-b@joh$5zmx(7B`Q8l+dNI%dAZpPc2e&T{X(DF`w?qPNKF#JOXn>r0W$CsBW>kP` z49mTt?-eP+Q_$dfUm>#EZ9@r=D4}%vyXDwX>8)Tvu5(CRx9CRW!w11P+%D6cF}wh; z3sqNg|`V*lhBziO%#UMqLVsw6p_)TSf_o-#1bO%^C2h7yz^+zlBNi1j0m9)t#= zVIW>JOE?HK%knI^dS{4Xo9-IKs3NS1;k z0a6{PA_~&B8;*dxI98%KfI#edTTE2cssy)k7^@wvU&>(!6MRJuhxm5u7n+MAf`m@o zuxST*-vRm6NF+AI5X46LAc9*KGXy}8v#eDJAqvwj^b*Ut-8g6u0N$ZF;1PeGF+|~+ z3hs3P9c8r)sg^iT!G-7~3JdrH^AZ}=EJq=)ki72o_P{uxZdM>)0AaH5{VubUeafd1 z!?K{K0LUS=k|lEIMM&q=Km*~&d#wC~PN}MBw^D?P+z6<#MMb-+{8%%20LKT zm~T;uBt2WO$ZR4`8GNYHsS*1%w=KTV8q5(7z``*d0D^k%W)fX6LhV=m=*zr5_u`yR0AAaK<9v|$JN9B6k8GBxZWg%xuO0gn0r6{P~5aqoY} zhYAs>#W+)VD#3s$L*1G6zi}j(Gi9FL28f(;ND(NRRjveglOEaPHY;%xqx}z>f!^gf z4YO<_jrc#19^@h?kQjP^{dG4z5-)*aB(KBw^yj_+E0x5s`r~Df*NQ9!M(Dsz2qHPr zpF#lm9c~u4mNEeD;?F|m2Z&)@M;OW6zzQa6fF4z~uDiGZgMca+?aU{ngcr>Z+ z9!bk_Q=!`t0?jl)BXr#22_B#+3h`~5aOIQWNttZ=_{(lc35wM1vyc$=su1!6^{C#T zE!$7vr~WD6cSzY8t_2vrLP2e(@evX`$|>mlgH4cya@PkEH(F3dPQ=;WpmHDn2Ra)x z1oG9Q3d|tx@S2iY5YYk}-~~wUq*N6H{JM&aCIJKh2HLj3MKf!gt1!O_sKgmmQd6A) zE-s(^4Ff69<76r&Ad%;o0gHCv5hYp&a~Yx4CPIybv$MoA8p;M4(_J3b&a zI8P7A*oFytoWfHE^7KH}J6;nz6v6szD7Ek{p1ixkN`-zI$rGIC5#~qlxAF3TMCwaC zJ(#9bzOrx!W0XZ8k;-8j3xCV>FGOQ3yc3|c`TgRO?nNAl1gdIeD|GcY%M%0WY3*O{^i8D%ZzNg(&%WzM)f*4>&D4m}+*g1<;zrlz9%Zvxgf8F2;#G z6_;v|^l;;bLQYUD5IIZe;ZDNUNxq&jv?OH=$X-EP?sKt+4%Axb2`hj_!uJIF*$InN zIv+m%OVA=EFL(=zI#_7hKr^YMzCv~A4Nf2!p}^t?>KzpU+m8Z<6vTOrff{NscRln2 z|4B5cWFi3wloX*9kfvY6f{yGyG2cMp~S9o{B*cpZRKG=k@429Oec6m zl3s00J!Bao)ZXC9Kw41bZW|R4dYrJ}%2EVv*vMJxki$EW0Nb2ZI)_nZAODw}%uoMx zVdEe4<>P=F#YwxcaVsF*b%DFE@eE6%36Un5lmZHjDRV9Zd~P9$J4(2v9^lmmEaSF{ zbSL6TSkW3qsxML)=tCI#;|_S-Wh-%B9TN#b;IMliTuS1lBTIoZ!8m2$`(A}VfO={< z;FKE{g{G6xoPtT$FeE8AEMaD1=;tzRov2rP{&fh{+=d(oV=lmLjV@Fc3BANlY#oq3 zwaFptVO1dQ#75FX9q#?{A-HB*-O5S$CTJjKIGffka8rfZO+XMNZ4SC1hUztYN@}OU z)i6Gc6d8gGqDZyNn8cC0gFQh!Cpti=^M3Q{E&|wl$t+-mfMeYYV7Q1GhunA?eG}nD zI|OqjcMZ~Q(c}vW=7hHe;fDa1k$^Rs`rz4T=6Zy?ag*&rt1_H30Ms)=b^+79XWLW$ zEpl_kuNUnPHS@3X=kCHS)7O=ZlSCht-vHrc<8=pa(wNM8TcUByp#U*4-vOIDpLw?j z#&ZqY^*#kmoc!#t&Jhy9BM;-jh(f>UvZQ$AhG83u$027Uw_ELa>5ur0uo^2z?buj{f9_sEtvF{G&|JL7y;SQd*HK zc*6xRkupxDG7MK?1m62Gq2B;6A=czAcoMjv7K(eUCdGJCk&h`u2uC3R$Y{^1u)R=? zOI5%SO;`&aXkQkvkkZWY+>eOP|8+e#>%sX8)tvXt|J1bwFgGB!KsnhiZ&2=l36%rN zO%N0W6r~^PencP#mkYz<7Ka4tO%ZNJ1)?TmE({AOMhQ?Pp)Hr!M3M{RCVKxZNXSVy`tgs9fe-1x^bj`s`Efo= z=L zNheirCg~bDl%o(D;CPxl6jtVj25rj_21T)O9lsb6ULZppJ{|-yNumtlA>acBeh$e{ zM7P_HQE_Wc0&NiTUqy$l=%4*-=OJst4?=c};}Ir19&9>>_y)`;k{${wf0%?&!xJHk z$x{i5_{}09LBwnDd1r`KA3U8?!J?CXZ{`aXoz+kYUkR&2@nCphxX2J*z$BbU3_%&T+~vxnBr5_u zV*}FHNmf?+CimI)6FAazPn<`NYhoM+ z7C0q92DgO-i#S}~#u$RNP{fKJfR~O$k&O^mi{6pz@kn2tz z)#j_D-hX+gd7;eBaqFkrS+mC9k*k2jTixxNHQuf9ijvI}3+=d?we24<$avS>sFWk3 z2q^QF08-R~N}|ZqN7C(J7!cz%icZ5ffV2~*mjF%zOixHuy z3&W8R=m#|GFe)F;^W>>7Y=_NFkU7+tH3ta=nwaInD7z~GZja{2E$|p^XSoEHfr#ZC zP)fvQ60Ziv5TVgx1&fk|n(~yyLXaJMpDRhfWTaeBN#6K4}o+6m9VR!*T|C~8S zFHmdY4Bo$Ij?u$3W<+t@apw5zNyD?GKoHLy2fuMs-$ob@#VN)_CUEGOnxD9xOc04g zF0g-#{(sIK`wEb4;zs%R%rRzxypC7}*>UFBfU;_*y&{}Bb~sM%Nn8UVwgBZ^a_E%< z&%;e>NLRtpRF1d~6dnmh+UH-ubtWJ~xzt*Opv4z*4#Rn>a!LPd@y@n~JNQDBCQ&5eu`nnGB;55>Od;#95 zOVk8Y>K|dj%SjSd2-Kj2`vIP$0A7=ix8Og@11ox!r27Go`5OY}??}&;?IRiZr*PY? zXl4*~k4Mz}vpQN@OXp&cy*Q&i^&s!W<%^=^|MkEYP^$ zX8?F6kCPO@$>uuvX#!IYnAxFAZ?ttfzxp@kr5w6bxr@98Dh!_~=pk|AfjMq@INyvG tJ-Fh!Yu7F;R-3Jk1N^ZEa)rBh`OBC`POS@pLiw&;vNB57(ytgk|3BeSKMVi> literal 0 HcmV?d00001 From 610562947ec1a8a1cebf4054b6a469cf44f8dd3d Mon Sep 17 00:00:00 2001 From: Prajwal-Microsoft Date: Thu, 19 Feb 2026 12:34:29 +0530 Subject: [PATCH 44/49] docs: Update README with new accelerators and playbook links Added new sections for Document Processing Accelerator and GPT-RAG Accelerator, along with links and descriptions. Updated playbook information for AI and Data Engineering. --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 826f60e9..ecc27d2c 100644 --- a/README.md +++ b/README.md @@ -194,11 +194,21 @@ Check out similar solution accelerators | [Conversation knowledge mining](https://github.com/microsoft/Conversation-Knowledge-Mining-Solution-Accelerator) | Derive insights from volumes of conversational data using generative AI. It offers key phrase extraction, topic modeling, and interactive chat experiences through an intuitive web interface. | | [Content processing](https://github.com/microsoft/content-processing-solution-accelerator) | Programmatically extract data and apply schemas to unstructured documents across text-based and multi-modal content using Azure AI Foundry, Azure OpenAI, Azure AI Content Understanding, and Azure Cosmos DB. | | [Build your own copilot - client advisor](https://github.com/microsoft/build-your-own-copilot-solution-accelerator) | This copilot helps client advisors to save time and prepare relevant discussion topics for scheduled meetings. It provides an overview of daily client meetings with seamless navigation between viewing client profiles and chatting with structured data. | - +|[Document Processing Accelerator](https://github.com/Azure/doc-proc-solution-accelerator/) | Modular document AI pipeline that automatically extracts, analyzes, and indexes information from unstructured documents (PDFs, images, etc.) at scale. It offers plug-and-play components for OCR, classification, summarization, and integration to search or chatbots – speeding up data ingestion with enterprise security.| +|[GPT-RAG Accelerator](https://github.com/Azure/gpt-rag)| Secure enterprise GPT assistant framework that uses Retrieval-Augmented Generation to ground answers on your data. It provides a ready architecture (Azure OpenAI + knowledge search) for building AI chatbots that “know” your enterprise content, with built-in security and scalability.|
+💡 Want to get familiar with Microsoft's AI and Data Engineering best practices? Check out our playbooks to learn more + +| Playbook | Description | +|:---|:---| +| [AI playbook](https://learn.microsoft.com/en-us/ai/playbook/) | The Artificial Intelligence (AI) Playbook provides enterprise software engineers with solutions, capabilities, and code developed to solve real-world AI problems. | +| [Data playbook](https://learn.microsoft.com/en-us/data-engineering/playbook/understanding-data-playbook) | The data playbook provides enterprise software engineers with solutions which contain code developed to solve real-world problems. Everything in the playbook is developed with, and validated by, some of Microsoft's largest and most influential customers and partners. | + +
+ ## Provide feedback Have questions, find a bug, or want to request a feature? [Submit a new issue](https://github.com/microsoft/document-knowledge-mining-solution-accelerator/issues) on this repo and we'll connect. From 8968421a686e0369a10f31f95314c3433da41fb0 Mon Sep 17 00:00:00 2001 From: Vamshi-Microsoft Date: Mon, 2 Mar 2026 10:56:28 +0530 Subject: [PATCH 45/49] Added Input Validation and Mapping Inputs to Env --- .github/workflows/deploy-linux.yml | 158 ++++++++++++++++- .github/workflows/deploy-orchestrator.yml | 2 +- .github/workflows/job-cleanup-deployment.yml | 93 ++++++++++ .github/workflows/job-deploy-linux.yml | 166 ++++++++++++++--- .github/workflows/job-deploy.yml | 176 +++++++++++++++++-- 5 files changed, 549 insertions(+), 46 deletions(-) diff --git a/.github/workflows/deploy-linux.yml b/.github/workflows/deploy-linux.yml index 64c8ce29..f31ed3fc 100644 --- a/.github/workflows/deploy-linux.yml +++ b/.github/workflows/deploy-linux.yml @@ -69,16 +69,158 @@ on: type: string jobs: + validate-inputs: + name: Validate Input Parameters + runs-on: ubuntu-latest + outputs: + validation_passed: ${{ steps.validate.outputs.passed }} + azure_location: ${{ steps.validate.outputs.azure_location }} + resource_group_name: ${{ steps.validate.outputs.resource_group_name }} + waf_enabled: ${{ steps.validate.outputs.waf_enabled }} + exp: ${{ steps.validate.outputs.exp }} + cleanup_resources: ${{ steps.validate.outputs.cleanup_resources }} + run_e2e_tests: ${{ steps.validate.outputs.run_e2e_tests }} + azure_env_log_analytics_workspace_id: ${{ steps.validate.outputs.azure_env_log_analytics_workspace_id }} + existing_webapp_url: ${{ steps.validate.outputs.existing_webapp_url }} + + steps: + - name: Validate Workflow Input Parameters + id: validate + shell: bash + env: + INPUT_AZURE_LOCATION: ${{ github.event.inputs.azure_location }} + INPUT_RESOURCE_GROUP_NAME: ${{ github.event.inputs.resource_group_name }} + INPUT_WAF_ENABLED: ${{ github.event.inputs.waf_enabled }} + INPUT_EXP: ${{ github.event.inputs.EXP }} + INPUT_CLEANUP_RESOURCES: ${{ github.event.inputs.cleanup_resources }} + INPUT_RUN_E2E_TESTS: ${{ github.event.inputs.run_e2e_tests }} + INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ github.event.inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }} + INPUT_EXISTING_WEBAPP_URL: ${{ github.event.inputs.existing_webapp_url }} + + run: | + echo "🔍 Validating workflow input parameters..." + VALIDATION_FAILED=false + + # Validate azure_location (Azure region format) + LOCATION="${INPUT_AZURE_LOCATION:-australiaeast}" + + if [[ ! "$LOCATION" =~ ^[a-z0-9]+$ ]]; then + echo "❌ ERROR: azure_location '$LOCATION' is invalid. Must contain only lowercase letters and numbers" + VALIDATION_FAILED=true + else + echo "✅ azure_location: '$LOCATION' is valid" + fi + + # Validate resource_group_name (Azure naming convention, optional) + if [[ -n "$INPUT_RESOURCE_GROUP_NAME" ]]; then + if [[ ! "$INPUT_RESOURCE_GROUP_NAME" =~ ^[a-zA-Z0-9._\(\)-]+$ ]] || [[ "$INPUT_RESOURCE_GROUP_NAME" =~ \.$ ]]; then + echo "❌ ERROR: resource_group_name '$INPUT_RESOURCE_GROUP_NAME' is invalid. Must contain only alphanumerics, periods, underscores, hyphens, and parentheses. Cannot end with period." + VALIDATION_FAILED=true + elif [[ ${#INPUT_RESOURCE_GROUP_NAME} -gt 90 ]]; then + echo "❌ ERROR: resource_group_name '$INPUT_RESOURCE_GROUP_NAME' exceeds 90 characters (length: ${#INPUT_RESOURCE_GROUP_NAME})" + VALIDATION_FAILED=true + else + echo "✅ resource_group_name: '$INPUT_RESOURCE_GROUP_NAME' is valid" + fi + else + echo "✅ resource_group_name: Not provided (will be auto-generated)" + fi + + # Validate waf_enabled (boolean) + WAF_ENABLED="${INPUT_WAF_ENABLED:-false}" + if [[ "$WAF_ENABLED" != "true" && "$WAF_ENABLED" != "false" ]]; then + echo "❌ ERROR: waf_enabled must be 'true' or 'false', got: '$WAF_ENABLED'" + VALIDATION_FAILED=true + else + echo "✅ waf_enabled: '$WAF_ENABLED' is valid" + fi + + # Validate EXP (boolean) + EXP_ENABLED="${INPUT_EXP:-false}" + if [[ "$EXP_ENABLED" != "true" && "$EXP_ENABLED" != "false" ]]; then + echo "❌ ERROR: EXP must be 'true' or 'false', got: '$EXP_ENABLED'" + VALIDATION_FAILED=true + else + echo "✅ EXP: '$EXP_ENABLED' is valid" + fi + + # Validate cleanup_resources (boolean) + CLEANUP_RESOURCES="${INPUT_CLEANUP_RESOURCES:-false}" + if [[ "$CLEANUP_RESOURCES" != "true" && "$CLEANUP_RESOURCES" != "false" ]]; then + echo "❌ ERROR: cleanup_resources must be 'true' or 'false', got: '$CLEANUP_RESOURCES'" + VALIDATION_FAILED=true + else + echo "✅ cleanup_resources: '$CLEANUP_RESOURCES' is valid" + fi + + # Validate run_e2e_tests (specific allowed values) + TEST_OPTION="${INPUT_RUN_E2E_TESTS:-GoldenPath-Testing}" + if [[ "$TEST_OPTION" != "GoldenPath-Testing" && "$TEST_OPTION" != "Smoke-Testing" && "$TEST_OPTION" != "None" ]]; then + echo "❌ ERROR: run_e2e_tests must be one of: GoldenPath-Testing, Smoke-Testing, None, got: '$TEST_OPTION'" + VALIDATION_FAILED=true + else + echo "✅ run_e2e_tests: '$TEST_OPTION' is valid" + fi + + # Validate AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID (optional, Azure Resource ID format) + if [[ -n "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID" ]]; then + if [[ ! "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/[Mm]icrosoft\.[Oo]perational[Ii]nsights/[Ww]orkspaces/[^/]+$ ]]; then + echo "❌ ERROR: AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID is invalid. Must be a valid Azure Resource ID format:" + echo " /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.OperationalInsights/workspaces/{workspaceName}" + echo " Got: '$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID'" + VALIDATION_FAILED=true + else + echo "✅ AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: Valid Resource ID format" + fi + else + echo "✅ AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: Not provided (optional)" + fi + + # Validate existing_webapp_url (optional, must start with https) + if [[ -n "$INPUT_EXISTING_WEBAPP_URL" ]]; then + if [[ ! "$INPUT_EXISTING_WEBAPP_URL" =~ ^https:// ]]; then + echo "❌ ERROR: existing_webapp_url must start with 'https://', got: '$INPUT_EXISTING_WEBAPP_URL'" + VALIDATION_FAILED=true + else + echo "✅ existing_webapp_url: '$INPUT_EXISTING_WEBAPP_URL' is valid" + fi + else + echo "✅ existing_webapp_url: Not provided (will perform deployment)" + fi + + # Fail workflow if any validation failed + if [[ "$VALIDATION_FAILED" == "true" ]]; then + echo "" + echo "❌ Parameter validation failed. Please correct the errors above and try again." + exit 1 + fi + + echo "" + echo "✅ All input parameters validated successfully!" + + # Output validated values + echo "passed=true" >> $GITHUB_OUTPUT + echo "azure_location=$LOCATION" >> $GITHUB_OUTPUT + echo "resource_group_name=$INPUT_RESOURCE_GROUP_NAME" >> $GITHUB_OUTPUT + echo "waf_enabled=$WAF_ENABLED" >> $GITHUB_OUTPUT + echo "exp=$EXP_ENABLED" >> $GITHUB_OUTPUT + echo "cleanup_resources=$CLEANUP_RESOURCES" >> $GITHUB_OUTPUT + echo "run_e2e_tests=$TEST_OPTION" >> $GITHUB_OUTPUT + echo "azure_env_log_analytics_workspace_id=$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID" >> $GITHUB_OUTPUT + echo "existing_webapp_url=$INPUT_EXISTING_WEBAPP_URL" >> $GITHUB_OUTPUT + Run: + needs: validate-inputs + if: needs.validate-inputs.outputs.validation_passed == 'true' uses: ./.github/workflows/deploy-orchestrator.yml with: - azure_location: ${{ github.event.inputs.azure_location || 'australiaeast' }} - resource_group_name: ${{ github.event.inputs.resource_group_name || '' }} - waf_enabled: ${{ github.event.inputs.waf_enabled == 'true' }} - EXP: ${{ github.event.inputs.EXP == 'true' }} - cleanup_resources: ${{ github.event.inputs.cleanup_resources == 'true' }} - run_e2e_tests: ${{ github.event.inputs.run_e2e_tests || 'GoldenPath-Testing' }} - AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ github.event.inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID || '' }} - existing_webapp_url: ${{ github.event.inputs.existing_webapp_url || '' }} + azure_location: ${{ needs.validate-inputs.outputs.azure_location || 'australiaeast' }} + resource_group_name: ${{ needs.validate-inputs.outputs.resource_group_name || '' }} + waf_enabled: ${{ needs.validate-inputs.outputs.waf_enabled == 'true' }} + EXP: ${{ needs.validate-inputs.outputs.exp == 'true' }} + cleanup_resources: ${{ needs.validate-inputs.outputs.cleanup_resources == 'true' }} + run_e2e_tests: ${{ needs.validate-inputs.outputs.run_e2e_tests || 'GoldenPath-Testing' }} + AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ needs.validate-inputs.outputs.azure_env_log_analytics_workspace_id || '' }} + existing_webapp_url: ${{ needs.validate-inputs.outputs.existing_webapp_url || '' }} trigger_type: ${{ github.event_name }} secrets: inherit diff --git a/.github/workflows/deploy-orchestrator.yml b/.github/workflows/deploy-orchestrator.yml index b42ca2b8..1b8e1cbd 100644 --- a/.github/workflows/deploy-orchestrator.yml +++ b/.github/workflows/deploy-orchestrator.yml @@ -96,7 +96,7 @@ jobs: secrets: inherit cleanup-deployment: - if: "!cancelled() && needs.deploy.result == 'success' && needs.deploy.outputs.RESOURCE_GROUP_NAME != '' && inputs.existing_webapp_url == '' && (inputs.trigger_type != 'workflow_dispatch' || inputs.cleanup_resources)" + if: "!cancelled() && needs.deploy.outputs.RESOURCE_GROUP_NAME != '' && inputs.existing_webapp_url == '' && (inputs.trigger_type != 'workflow_dispatch' || inputs.cleanup_resources)" needs: [deploy, e2e-test] uses: ./.github/workflows/job-cleanup-deployment.yml with: diff --git a/.github/workflows/job-cleanup-deployment.yml b/.github/workflows/job-cleanup-deployment.yml index 30e518e3..428e4da5 100644 --- a/.github/workflows/job-cleanup-deployment.yml +++ b/.github/workflows/job-cleanup-deployment.yml @@ -48,6 +48,99 @@ jobs: ENV_NAME: ${{ inputs.ENV_NAME }} IMAGE_TAG: ${{ inputs.IMAGE_TAG }} steps: + - name: Validate Workflow Input Parameters + shell: bash + env: + INPUT_TRIGGER_TYPE: ${{ inputs.trigger_type }} + INPUT_CLEANUP_RESOURCES: ${{ inputs.cleanup_resources }} + INPUT_EXISTING_WEBAPP_URL: ${{ inputs.existing_webapp_url }} + INPUT_RESOURCE_GROUP_NAME: ${{ inputs.RESOURCE_GROUP_NAME }} + INPUT_AZURE_LOCATION: ${{ inputs.AZURE_LOCATION }} + INPUT_AZURE_ENV_OPENAI_LOCATION: ${{ inputs.AZURE_ENV_OPENAI_LOCATION }} + INPUT_ENV_NAME: ${{ inputs.ENV_NAME }} + INPUT_IMAGE_TAG: ${{ inputs.IMAGE_TAG }} + run: | + echo "🔍 Validating workflow input parameters..." + VALIDATION_FAILED=false + + # Validate trigger_type (required - alphanumeric with underscores) + if [[ -z "$INPUT_TRIGGER_TYPE" ]]; then + echo "❌ ERROR: trigger_type is required but was not provided" + VALIDATION_FAILED=true + elif [[ ! "$INPUT_TRIGGER_TYPE" =~ ^[a-zA-Z0-9_]+$ ]]; then + echo "❌ ERROR: trigger_type '$INPUT_TRIGGER_TYPE' is invalid. Must contain only alphanumeric characters and underscores" + VALIDATION_FAILED=true + fi + + # Validate cleanup_resources (boolean) + if [[ "$INPUT_CLEANUP_RESOURCES" != "true" && "$INPUT_CLEANUP_RESOURCES" != "false" ]]; then + echo "❌ ERROR: cleanup_resources must be 'true' or 'false', got '$INPUT_CLEANUP_RESOURCES'" + VALIDATION_FAILED=true + fi + + # Validate existing_webapp_url (optional - must start with https if provided) + if [[ -n "$INPUT_EXISTING_WEBAPP_URL" ]]; then + if [[ ! "$INPUT_EXISTING_WEBAPP_URL" =~ ^https:// ]]; then + echo "❌ ERROR: existing_webapp_url must start with 'https://', got '$INPUT_EXISTING_WEBAPP_URL'" + VALIDATION_FAILED=true + fi + fi + + # Validate RESOURCE_GROUP_NAME (required - Azure resource group naming convention) + if [[ -z "$INPUT_RESOURCE_GROUP_NAME" ]]; then + echo "❌ ERROR: RESOURCE_GROUP_NAME is required but was not provided" + VALIDATION_FAILED=true + elif [[ ! "$INPUT_RESOURCE_GROUP_NAME" =~ ^[a-zA-Z0-9._\(\)-]+$ ]] || [[ "$INPUT_RESOURCE_GROUP_NAME" =~ \.$ ]]; then + echo "❌ ERROR: RESOURCE_GROUP_NAME is invalid. Must contain only alphanumerics, periods, underscores, hyphens, and parentheses. Cannot end with period." + VALIDATION_FAILED=true + elif [[ ${#INPUT_RESOURCE_GROUP_NAME} -gt 90 ]]; then + echo "❌ ERROR: RESOURCE_GROUP_NAME exceeds 90 characters" + VALIDATION_FAILED=true + fi + + # Validate AZURE_LOCATION (required - Azure region format) + if [[ -z "$INPUT_AZURE_LOCATION" ]]; then + echo "❌ ERROR: AZURE_LOCATION is required but was not provided" + VALIDATION_FAILED=true + elif [[ ! "$INPUT_AZURE_LOCATION" =~ ^[a-z0-9]+$ ]]; then + echo "❌ ERROR: AZURE_LOCATION '$INPUT_AZURE_LOCATION' is invalid. Must contain only lowercase letters and numbers" + VALIDATION_FAILED=true + fi + + # Validate AZURE_ENV_OPENAI_LOCATION (required - Azure region format) + if [[ -z "$INPUT_AZURE_ENV_OPENAI_LOCATION" ]]; then + echo "❌ ERROR: AZURE_ENV_OPENAI_LOCATION is required but was not provided" + VALIDATION_FAILED=true + elif [[ ! "$INPUT_AZURE_ENV_OPENAI_LOCATION" =~ ^[a-z0-9]+$ ]]; then + echo "❌ ERROR: AZURE_ENV_OPENAI_LOCATION '$INPUT_AZURE_ENV_OPENAI_LOCATION' is invalid. Must contain only lowercase letters and numbers" + VALIDATION_FAILED=true + fi + + # Validate ENV_NAME (required - alphanumeric with underscores and hyphens) + if [[ -z "$INPUT_ENV_NAME" ]]; then + echo "❌ ERROR: ENV_NAME is required but was not provided" + VALIDATION_FAILED=true + elif [[ ! "$INPUT_ENV_NAME" =~ ^[a-zA-Z0-9_-]+$ ]]; then + echo "❌ ERROR: ENV_NAME '$INPUT_ENV_NAME' is invalid. Must contain only alphanumeric characters, underscores, and hyphens" + VALIDATION_FAILED=true + fi + + # Validate IMAGE_TAG (required - Docker tag pattern) + if [[ -z "$INPUT_IMAGE_TAG" ]]; then + echo "❌ ERROR: IMAGE_TAG is required but was not provided" + VALIDATION_FAILED=true + elif [[ ! "$INPUT_IMAGE_TAG" =~ ^[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}$ ]]; then + echo "❌ ERROR: IMAGE_TAG '$INPUT_IMAGE_TAG' is invalid. Must be a valid Docker tag (alphanumeric start, up to 128 chars)" + VALIDATION_FAILED=true + fi + + if [[ "$VALIDATION_FAILED" == "true" ]]; then + echo "❌ Input validation failed. Please check the errors above." + exit 1 + fi + + echo "✅ All input parameters validated successfully" + - name: Setup Azure CLI shell: bash run: | diff --git a/.github/workflows/job-deploy-linux.yml b/.github/workflows/job-deploy-linux.yml index b8066ec5..28708215 100644 --- a/.github/workflows/job-deploy-linux.yml +++ b/.github/workflows/job-deploy-linux.yml @@ -41,13 +41,129 @@ jobs: outputs: WEB_APPURL: ${{ steps.get_webapp_url.outputs.WEB_APPURL }} steps: + - name: Validate Workflow Input Parameters + shell: bash + env: + INPUT_ENV_NAME: ${{ inputs.ENV_NAME }} + INPUT_AZURE_ENV_OPENAI_LOCATION: ${{ inputs.AZURE_ENV_OPENAI_LOCATION }} + INPUT_AZURE_LOCATION: ${{ inputs.AZURE_LOCATION }} + INPUT_RESOURCE_GROUP_NAME: ${{ inputs.RESOURCE_GROUP_NAME }} + INPUT_IMAGE_TAG: ${{ inputs.IMAGE_TAG }} + INPUT_EXP: ${{ inputs.EXP }} + INPUT_WAF_ENABLED: ${{ inputs.WAF_ENABLED }} + INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }} + run: | + echo "🔍 Validating workflow input parameters..." + VALIDATION_FAILED=false + + # Validate ENV_NAME (required - alphanumeric) + if [[ -z "$INPUT_ENV_NAME" ]]; then + echo "❌ ERROR: ENV_NAME is required but was not provided" + VALIDATION_FAILED=true + elif [[ ! "$INPUT_ENV_NAME" =~ ^[a-zA-Z0-9_-]+$ ]]; then + echo "❌ ERROR: ENV_NAME '$INPUT_ENV_NAME' is invalid. Must contain only alphanumeric characters, underscores, and hyphens" + VALIDATION_FAILED=true + else + echo "✅ ENV_NAME: '$INPUT_ENV_NAME' is valid" + fi + + # Validate AZURE_ENV_OPENAI_LOCATION (required - Azure region format) + if [[ -z "$INPUT_AZURE_ENV_OPENAI_LOCATION" ]]; then + echo "❌ ERROR: AZURE_ENV_OPENAI_LOCATION is required but was not provided" + VALIDATION_FAILED=true + elif [[ ! "$INPUT_AZURE_ENV_OPENAI_LOCATION" =~ ^[a-z0-9]+$ ]]; then + echo "❌ ERROR: AZURE_ENV_OPENAI_LOCATION '$INPUT_AZURE_ENV_OPENAI_LOCATION' is invalid. Must contain only lowercase letters and numbers (e.g., 'australiaeast', 'westus2')" + VALIDATION_FAILED=true + else + echo "✅ AZURE_ENV_OPENAI_LOCATION: '$INPUT_AZURE_ENV_OPENAI_LOCATION' is valid" + fi + + # Validate AZURE_LOCATION (required - Azure region format) + if [[ -z "$INPUT_AZURE_LOCATION" ]]; then + echo "❌ ERROR: AZURE_LOCATION is required but was not provided" + VALIDATION_FAILED=true + elif [[ ! "$INPUT_AZURE_LOCATION" =~ ^[a-z0-9]+$ ]]; then + echo "❌ ERROR: AZURE_LOCATION '$INPUT_AZURE_LOCATION' is invalid. Must contain only lowercase letters and numbers (e.g., 'australiaeast', 'westus2')" + VALIDATION_FAILED=true + else + echo "✅ AZURE_LOCATION: '$INPUT_AZURE_LOCATION' is valid" + fi + + # Validate RESOURCE_GROUP_NAME (required - Azure resource group naming convention) + if [[ -z "$INPUT_RESOURCE_GROUP_NAME" ]]; then + echo "❌ ERROR: RESOURCE_GROUP_NAME is required but was not provided" + VALIDATION_FAILED=true + elif [[ ! "$INPUT_RESOURCE_GROUP_NAME" =~ ^[a-zA-Z0-9._\(\)-]+$ ]] || [[ "$INPUT_RESOURCE_GROUP_NAME" =~ \.$ ]]; then + echo "❌ ERROR: RESOURCE_GROUP_NAME '$INPUT_RESOURCE_GROUP_NAME' is invalid. Must contain only alphanumerics, periods, underscores, hyphens, and parentheses. Cannot end with period." + VALIDATION_FAILED=true + elif [[ ${#INPUT_RESOURCE_GROUP_NAME} -gt 90 ]]; then + echo "❌ ERROR: RESOURCE_GROUP_NAME '$INPUT_RESOURCE_GROUP_NAME' exceeds 90 characters" + VALIDATION_FAILED=true + else + echo "✅ RESOURCE_GROUP_NAME: '$INPUT_RESOURCE_GROUP_NAME' is valid" + fi + + # Validate IMAGE_TAG (required - Docker tag pattern) + if [[ -z "$INPUT_IMAGE_TAG" ]]; then + echo "❌ ERROR: IMAGE_TAG is required but was not provided" + VALIDATION_FAILED=true + elif [[ ! "$INPUT_IMAGE_TAG" =~ ^[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}$ ]]; then + echo "❌ ERROR: IMAGE_TAG '$INPUT_IMAGE_TAG' is invalid. Must start with alphanumeric or underscore, contain only alphanumerics, underscores, periods, hyphens, and be max 128 characters" + VALIDATION_FAILED=true + else + echo "✅ IMAGE_TAG: '$INPUT_IMAGE_TAG' is valid" + fi + + # Validate EXP (required - must be 'true' or 'false') + if [[ -z "$INPUT_EXP" ]]; then + echo "❌ ERROR: EXP is required but was not provided" + VALIDATION_FAILED=true + elif [[ "$INPUT_EXP" != "true" && "$INPUT_EXP" != "false" ]]; then + echo "❌ ERROR: EXP must be 'true' or 'false', got: '$INPUT_EXP'" + VALIDATION_FAILED=true + else + echo "✅ EXP: '$INPUT_EXP' is valid" + fi + + # Validate WAF_ENABLED (must be 'true' or 'false') + if [[ "$INPUT_WAF_ENABLED" != "true" && "$INPUT_WAF_ENABLED" != "false" ]]; then + echo "❌ ERROR: WAF_ENABLED must be 'true' or 'false', got: '$INPUT_WAF_ENABLED'" + VALIDATION_FAILED=true + else + echo "✅ WAF_ENABLED: '$INPUT_WAF_ENABLED' is valid" + fi + + # Validate AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID (optional - Azure Resource ID format) + if [[ -n "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID" ]]; then + if [[ ! "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/[Mm]icrosoft\.[Oo]perational[Ii]nsights/[Ww]orkspaces/[^/]+$ ]]; then + echo "❌ ERROR: AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID is invalid. Must be a valid Azure Resource ID format:" + echo " /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.OperationalInsights/workspaces/{workspaceName}" + echo " Got: '$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID'" + VALIDATION_FAILED=true + else + echo "✅ AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: Valid Resource ID format" + fi + fi + + # Fail workflow if any validation failed + if [[ "$VALIDATION_FAILED" == "true" ]]; then + echo "" + echo "❌ Parameter validation failed. Please correct the errors above and try again." + exit 1 + fi + + echo "" + echo "✅ All input parameters validated successfully!" + - name: Checkout Code uses: actions/checkout@v4 - name: Configure Parameters Based on WAF Setting shell: bash + env: + INPUT_WAF_ENABLED: ${{ inputs.WAF_ENABLED }} run: | - if [[ "${{ inputs.WAF_ENABLED }}" == "true" ]]; then + if [[ "$INPUT_WAF_ENABLED" == "true" ]]; then cp infra/main.waf.parameters.json infra/main.parameters.json echo "✅ Successfully copied WAF parameters to main parameters file" else @@ -114,28 +230,36 @@ jobs: - name: Deploy using azd up id: azd_deploy shell: pwsh + env: + INPUT_ENV_NAME: ${{ inputs.ENV_NAME }} + INPUT_AZURE_ENV_OPENAI_LOCATION: ${{ inputs.AZURE_ENV_OPENAI_LOCATION }} + INPUT_AZURE_LOCATION: ${{ inputs.AZURE_LOCATION }} + INPUT_RESOURCE_GROUP_NAME: ${{ inputs.RESOURCE_GROUP_NAME }} + INPUT_IMAGE_TAG: ${{ inputs.IMAGE_TAG }} + INPUT_EXP: ${{ inputs.EXP }} + INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }} run: | # Create azd environment - azd env new ${{ inputs.ENV_NAME }} --no-prompt + azd env new $env:INPUT_ENV_NAME --no-prompt # Set environment variables azd config set defaults.subscription ${{ secrets.AZURE_SUBSCRIPTION_ID }} azd env set AZURE_SUBSCRIPTION_ID="${{ secrets.AZURE_SUBSCRIPTION_ID }}" - azd env set AZURE_ENV_OPENAI_LOCATION="${{ inputs.AZURE_ENV_OPENAI_LOCATION }}" - azd env set AZURE_LOCATION="${{ inputs.AZURE_LOCATION }}" - azd env set AZURE_RESOURCE_GROUP="${{ inputs.RESOURCE_GROUP_NAME }}" - azd env set AZURE_ENV_IMAGE_TAG="${{ inputs.IMAGE_TAG }}" + azd env set AZURE_ENV_OPENAI_LOCATION="$env:INPUT_AZURE_ENV_OPENAI_LOCATION" + azd env set AZURE_LOCATION="$env:INPUT_AZURE_LOCATION" + azd env set AZURE_RESOURCE_GROUP="$env:INPUT_RESOURCE_GROUP_NAME" + azd env set AZURE_ENV_IMAGE_TAG="$env:INPUT_IMAGE_TAG" # Set AI model capacity parameters azd env set AZURE_ENV_MODEL_CAPACITY="150" azd env set AZURE_ENV_EMBEDDING_MODEL_CAPACITY="200" - if ("${{ inputs.EXP }}" -eq "true") { + if ($env:INPUT_EXP -eq "true") { Write-Host "✅ EXP ENABLED - Setting EXP parameters..." # Set EXP variables dynamically - if ("${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}" -ne "") { - $EXP_LOG_ANALYTICS_ID = "${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}" + if ($env:INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID -ne "") { + $EXP_LOG_ANALYTICS_ID = $env:INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID } else { $EXP_LOG_ANALYTICS_ID = "${{ secrets.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}" } @@ -153,6 +277,8 @@ jobs: - name: Get Deployment Outputs id: get_output + env: + INPUT_RESOURCE_GROUP_NAME: ${{ inputs.RESOURCE_GROUP_NAME }} run: | # Get outputs from azd azd env get-values --output json > /tmp/azd_output.json @@ -167,7 +293,7 @@ jobs: # Get AKS node resource group if AKS exists if [ -n "$AZURE_AKS_NAME" ]; then - krg_name=$(az aks show --name "$AZURE_AKS_NAME" --resource-group "${{ inputs.RESOURCE_GROUP_NAME }}" --query "nodeResourceGroup" -o tsv || echo "") + krg_name=$(az aks show --name "$AZURE_AKS_NAME" --resource-group "$INPUT_RESOURCE_GROUP_NAME" --query "nodeResourceGroup" -o tsv || echo "") if [ -n "$krg_name" ]; then echo "krg_name=$krg_name" >> $GITHUB_ENV echo "AKS node resource group: $krg_name" @@ -176,16 +302,6 @@ jobs: - name: Run Deployment Script with Input shell: pwsh - run: | - cd Deployment - $input = @" - ${{ secrets.EMAIL }} - yes - "@ - $input | pwsh ./resourcedeployment.ps1 - Write-Host "Resource Group: ${{ inputs.RESOURCE_GROUP_NAME }}" - Write-Host "AKS Cluster Name: ${{ env.AZURE_AKS_NAME }}" - Write-Host "AKS Node Resource Group: ${{ env.krg_name }}" env: # From GitHub secrets (for login) AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} @@ -212,6 +328,16 @@ jobs: AZ_GPT_EMBEDDING_MODEL_ID: ${{ env.AZ_GPT_EMBEDDING_MODEL_ID }} AZURE_APP_CONFIG_ENDPOINT: ${{ env.AZURE_APP_CONFIG_ENDPOINT }} AZURE_APP_CONFIG_NAME: ${{ env.AZURE_APP_CONFIG_NAME }} + run: | + cd Deployment + $input = @" + ${{ secrets.EMAIL }} + yes + "@ + $input | pwsh ./resourcedeployment.ps1 + Write-Host "Resource Group: $env:RESOURCE_GROUP_NAME" + Write-Host "AKS Cluster Name: $env:AZURE_AKS_NAME" + Write-Host "AKS Node Resource Group: $env:krg_name" - name: Retrieve Web App URL id: get_webapp_url diff --git a/.github/workflows/job-deploy.yml b/.github/workflows/job-deploy.yml index 40be0127..99ff9717 100644 --- a/.github/workflows/job-deploy.yml +++ b/.github/workflows/job-deploy.yml @@ -92,27 +92,154 @@ jobs: AZURE_ENV_OPENAI_LOCATION: ${{ steps.set_region.outputs.AZURE_ENV_OPENAI_LOCATION }} IMAGE_TAG: ${{ steps.determine_image_tag.outputs.IMAGE_TAG }} QUOTA_FAILED: ${{ steps.quota_failure_output.outputs.QUOTA_FAILED }} + EXP_ENABLED: ${{ steps.configure_exp.outputs.EXP_ENABLED }} steps: - - name: Validate EXP Configuration + - name: Validate Workflow Input Parameters shell: bash + env: + INPUT_TRIGGER_TYPE: ${{ inputs.trigger_type }} + INPUT_AZURE_LOCATION: ${{ inputs.azure_location }} + INPUT_RESOURCE_GROUP_NAME: ${{ inputs.resource_group_name }} + INPUT_WAF_ENABLED: ${{ inputs.waf_enabled }} + INPUT_EXP: ${{ inputs.EXP }} + INPUT_CLEANUP_RESOURCES: ${{ inputs.cleanup_resources }} + INPUT_RUN_E2E_TESTS: ${{ inputs.run_e2e_tests }} + INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }} + INPUT_EXISTING_WEBAPP_URL: ${{ inputs.existing_webapp_url }} + run: | + echo "🔍 Validating workflow input parameters..." + VALIDATION_FAILED=false + + # Validate trigger_type (required - alphanumeric with underscores) + if [[ -z "$INPUT_TRIGGER_TYPE" ]]; then + echo "❌ ERROR: trigger_type is required but was not provided" + VALIDATION_FAILED=true + elif [[ ! "$INPUT_TRIGGER_TYPE" =~ ^[a-zA-Z0-9_]+$ ]]; then + echo "❌ ERROR: trigger_type '$INPUT_TRIGGER_TYPE' is invalid. Must contain only alphanumeric characters and underscores" + VALIDATION_FAILED=true + else + echo "✅ trigger_type: '$INPUT_TRIGGER_TYPE' is valid" + fi + + # Validate azure_location (Azure region format) + if [[ -n "$INPUT_AZURE_LOCATION" ]]; then + if [[ ! "$INPUT_AZURE_LOCATION" =~ ^[a-z0-9]+$ ]]; then + echo "❌ ERROR: azure_location '$INPUT_AZURE_LOCATION' is invalid. Must contain only lowercase letters and numbers (e.g., 'australiaeast', 'westus2')" + VALIDATION_FAILED=true + else + echo "✅ azure_location: '$INPUT_AZURE_LOCATION' is valid" + fi + fi + + # Validate resource_group_name (Azure resource group naming convention) + if [[ -n "$INPUT_RESOURCE_GROUP_NAME" ]]; then + if [[ ! "$INPUT_RESOURCE_GROUP_NAME" =~ ^[a-zA-Z0-9._\(\)-]+$ ]] || [[ "$INPUT_RESOURCE_GROUP_NAME" =~ \.$ ]]; then + echo "❌ ERROR: resource_group_name '$INPUT_RESOURCE_GROUP_NAME' is invalid. Must contain only alphanumerics, periods, underscores, hyphens, and parentheses. Cannot end with period." + VALIDATION_FAILED=true + elif [[ ${#INPUT_RESOURCE_GROUP_NAME} -gt 90 ]]; then + echo "❌ ERROR: resource_group_name '$INPUT_RESOURCE_GROUP_NAME' exceeds 90 characters" + VALIDATION_FAILED=true + else + echo "✅ resource_group_name: '$INPUT_RESOURCE_GROUP_NAME' is valid" + fi + fi + + # Validate waf_enabled (boolean) + if [[ "$INPUT_WAF_ENABLED" != "true" && "$INPUT_WAF_ENABLED" != "false" ]]; then + echo "❌ ERROR: waf_enabled must be 'true' or 'false', got: '$INPUT_WAF_ENABLED'" + VALIDATION_FAILED=true + else + echo "✅ waf_enabled: '$INPUT_WAF_ENABLED' is valid" + fi + + # Validate EXP (boolean) + if [[ "$INPUT_EXP" != "true" && "$INPUT_EXP" != "false" ]]; then + echo "❌ ERROR: EXP must be 'true' or 'false', got: '$INPUT_EXP'" + VALIDATION_FAILED=true + else + echo "✅ EXP: '$INPUT_EXP' is valid" + fi + + # Validate cleanup_resources (boolean) + if [[ "$INPUT_CLEANUP_RESOURCES" != "true" && "$INPUT_CLEANUP_RESOURCES" != "false" ]]; then + echo "❌ ERROR: cleanup_resources must be 'true' or 'false', got: '$INPUT_CLEANUP_RESOURCES'" + VALIDATION_FAILED=true + else + echo "✅ cleanup_resources: '$INPUT_CLEANUP_RESOURCES' is valid" + fi + + # Validate run_e2e_tests (specific allowed values) + if [[ -n "$INPUT_RUN_E2E_TESTS" ]]; then + ALLOWED_VALUES=("None" "GoldenPath-Testing" "Smoke-Testing") + if [[ ! " ${ALLOWED_VALUES[@]} " =~ " ${INPUT_RUN_E2E_TESTS} " ]]; then + echo "❌ ERROR: run_e2e_tests '$INPUT_RUN_E2E_TESTS' is invalid. Allowed values: ${ALLOWED_VALUES[*]}" + VALIDATION_FAILED=true + else + echo "✅ run_e2e_tests: '$INPUT_RUN_E2E_TESTS' is valid" + fi + fi + + # Validate AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID (Azure Resource ID format) + if [[ -n "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID" ]]; then + if [[ ! "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/[Mm]icrosoft\.[Oo]perational[Ii]nsights/[Ww]orkspaces/[^/]+$ ]]; then + echo "❌ ERROR: AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID is invalid. Must be a valid Azure Resource ID format:" + echo " /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.OperationalInsights/workspaces/{workspaceName}" + echo " Got: '$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID'" + VALIDATION_FAILED=true + else + echo "✅ AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: Valid Resource ID format" + fi + fi + + # Validate existing_webapp_url (must start with https) + if [[ -n "$INPUT_EXISTING_WEBAPP_URL" ]]; then + if [[ ! "$INPUT_EXISTING_WEBAPP_URL" =~ ^https:// ]]; then + echo "❌ ERROR: existing_webapp_url must start with 'https://', got: '$INPUT_EXISTING_WEBAPP_URL'" + VALIDATION_FAILED=true + else + echo "✅ existing_webapp_url: '$INPUT_EXISTING_WEBAPP_URL' is valid" + fi + fi + + # Fail workflow if any validation failed + if [[ "$VALIDATION_FAILED" == "true" ]]; then + echo "" + echo "❌ Parameter validation failed. Please correct the errors above and try again." + exit 1 + fi + + echo "" + echo "✅ All input parameters validated successfully!" + + - name: Validate and Auto-Configure EXP + id: configure_exp + shell: bash + env: + INPUT_EXP: ${{ inputs.EXP }} + INPUT_LOG_ANALYTICS_WORKSPACE_ID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }} run: | echo "🔍 Validating EXP configuration..." - if [[ "${{ inputs.EXP }}" == "false" && -n "${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}" ]]; then - echo "⚠️ WARNING: EXP is disabled but Log Analytics Workspace ID was provided." - echo "The provided workspace ID will be ignored unless EXP is enabled." + EXP_ENABLED="false" + + if [[ "$INPUT_EXP" == "true" ]]; then + EXP_ENABLED="true" + echo "✅ EXP explicitly enabled by user input" + elif [[ -n "$INPUT_LOG_ANALYTICS_WORKSPACE_ID" ]]; then + echo "🔧 AUTO-ENABLING EXP: Log Analytics Workspace ID was provided but EXP was not explicitly enabled." echo "" - echo "Provided but unused:" - echo " - Azure Log Analytics Workspace ID: '${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}'" + echo "You provided values for:" + echo " - Azure Log Analytics Workspace ID: '$INPUT_LOG_ANALYTICS_WORKSPACE_ID'" echo "" - echo "To use these values, set EXP=true in your workflow dispatch." - elif [[ "${{ inputs.EXP }}" == "true" ]]; then - echo "✅ EXP is enabled" - else - echo "ℹ️ EXP is disabled" + echo "✅ Automatically enabling EXP to use these values." + EXP_ENABLED="true" fi + echo "EXP_ENABLED=$EXP_ENABLED" >> $GITHUB_ENV + echo "EXP_ENABLED=$EXP_ENABLED" >> $GITHUB_OUTPUT + echo "Final EXP status: $EXP_ENABLED" + - name: Checkout Code uses: actions/checkout@v4 @@ -171,6 +298,9 @@ jobs: - name: Set Deployment Region id: set_region shell: bash + env: + INPUT_TRIGGER_TYPE: ${{ inputs.trigger_type }} + INPUT_AZURE_LOCATION: ${{ inputs.azure_location }} run: | if [[ -z "$VALID_REGION" ]]; then echo "❌ ERROR: VALID_REGION is not set. The quota check script (Deployment/checkquota.ps1) must set this variable before this step runs." >&2 @@ -180,8 +310,8 @@ jobs: echo "AZURE_ENV_OPENAI_LOCATION=$VALID_REGION" >> $GITHUB_ENV echo "AZURE_ENV_OPENAI_LOCATION=$VALID_REGION" >> $GITHUB_OUTPUT - if [[ "${{ inputs.trigger_type }}" == "workflow_dispatch" && -n "${{ inputs.azure_location }}" ]]; then - USER_SELECTED_LOCATION="${{ inputs.azure_location }}" + if [[ "$INPUT_TRIGGER_TYPE" == "workflow_dispatch" && -n "$INPUT_AZURE_LOCATION" ]]; then + USER_SELECTED_LOCATION="$INPUT_AZURE_LOCATION" echo "Using user-selected Azure location: $USER_SELECTED_LOCATION" echo "AZURE_LOCATION=$USER_SELECTED_LOCATION" >> $GITHUB_ENV echo "AZURE_LOCATION=$USER_SELECTED_LOCATION" >> $GITHUB_OUTPUT @@ -194,11 +324,13 @@ jobs: - name: Generate Resource Group Name id: generate_rg_name shell: bash + env: + INPUT_RESOURCE_GROUP_NAME: ${{ inputs.resource_group_name }} run: | # Check if a resource group name was provided as input - if [[ -n "${{ inputs.resource_group_name }}" ]]; then - echo "Using provided Resource Group name: ${{ inputs.resource_group_name }}" - echo "RESOURCE_GROUP_NAME=${{ inputs.resource_group_name }}" >> $GITHUB_ENV + if [[ -n "$INPUT_RESOURCE_GROUP_NAME" ]]; then + echo "Using provided Resource Group name: $INPUT_RESOURCE_GROUP_NAME" + echo "RESOURCE_GROUP_NAME=$INPUT_RESOURCE_GROUP_NAME" >> $GITHUB_ENV else echo "Generating a unique resource group name..." ACCL_NAME="dkm" # Account name as specified @@ -270,6 +402,16 @@ jobs: - name: Display Workflow Configuration to GitHub Summary shell: bash + env: + INPUT_TRIGGER_TYPE: ${{ inputs.trigger_type }} + INPUT_AZURE_LOCATION: ${{ inputs.azure_location }} + INPUT_RESOURCE_GROUP_NAME: ${{ inputs.resource_group_name }} + STEP_EVENT_NAME: ${{ github.event_name }} + STEP_BRANCH_NAME: ${{ env.BRANCH_NAME }} + STEP_WAF_ENABLED: ${{ env.WAF_ENABLED }} + STEP_EXP: ${{ env.EXP }} + STEP_RUN_E2E_TESTS: ${{ env.RUN_E2E_TESTS }} + STEP_CLEANUP_RESOURCES: ${{ env.CLEANUP_RESOURCES }} run: | echo "## 📋 Workflow Configuration Summary" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY @@ -311,7 +453,7 @@ jobs: AZURE_LOCATION: ${{ needs.azure-setup.outputs.AZURE_LOCATION }} RESOURCE_GROUP_NAME: ${{ needs.azure-setup.outputs.RESOURCE_GROUP_NAME }} IMAGE_TAG: ${{ needs.azure-setup.outputs.IMAGE_TAG }} - EXP: ${{ inputs.EXP || 'false' }} + EXP: ${{ needs.azure-setup.outputs.EXP_ENABLED || inputs.EXP || 'false' }} WAF_ENABLED: ${{ inputs.waf_enabled == true && 'true' || 'false' }} AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }} secrets: inherit From 2bf44414c2b3dac984402aad75f7b83cb5df5bb9 Mon Sep 17 00:00:00 2001 From: Vamshi-Microsoft Date: Mon, 2 Mar 2026 11:25:46 +0530 Subject: [PATCH 46/49] Migrated GitHub Actions authentication from client secrets to OIDC --- .github/workflows/CI.yml | 38 +++++++++---------- .github/workflows/deploy-orchestrator.yml | 3 +- .../{deploy-linux.yml => deploy-v2.yml} | 5 +++ .github/workflows/job-cleanup-deployment.yml | 10 +++-- .github/workflows/job-deploy-linux.yml | 25 ++++++++---- .github/workflows/job-deploy.yml | 14 +++---- .github/workflows/test-automation-v2.yml | 13 +++---- .github/workflows/test-automation.yml | 3 -- Deployment/checkquota.ps1 | 20 ++++------ Deployment/resourcedeployment.ps1 | 13 ++++--- 10 files changed, 79 insertions(+), 65 deletions(-) rename .github/workflows/{deploy-linux.yml => deploy-v2.yml} (99%) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 3d72c38f..dab5903e 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -16,6 +16,7 @@ on: schedule: - cron: "0 10,22 * * *" # Runs at 10:00 AM and 10:00 PM GMT permissions: + id-token: write contents: read actions: read env: @@ -25,6 +26,7 @@ env: jobs: deploy: runs-on: ubuntu-latest + environment: production outputs: RESOURCE_GROUP_NAME: ${{ steps.get_webapp_url.outputs.RESOURCE_GROUP_NAME }} KUBERNETES_RESOURCE_GROUP_NAME: ${{ steps.get_webapp_url.outputs.KUBERNETES_RESOURCE_GROUP_NAME }} @@ -78,6 +80,14 @@ jobs: with: driver: docker + - name: Login to Azure + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + enable-AzPSSession: true + - name: Run Quota Check id: quota-check shell: pwsh @@ -105,9 +115,6 @@ jobs: } env: AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} - AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} - AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} GPT_MIN_CAPACITY: ${{ env.GPT_CAPACITY }} TEXT_EMBEDDING_MIN_CAPACITY: ${{ env.TEXT_EMBEDDING_CAPACITY }} AZURE_REGIONS: "${{ vars.AZURE_REGIONS }}" @@ -158,11 +165,6 @@ jobs: echo "RESOURCE_GROUP_NAME=${UNIQUE_RG_NAME}" >> $GITHUB_ENV echo "Generated RESOURCE_GROUP_NAME: ${UNIQUE_RG_NAME}" - - name: Login to Azure - run: | - az login --service-principal -u ${{ secrets.AZURE_CLIENT_ID }} -p ${{ secrets.AZURE_CLIENT_SECRET }} --tenant ${{ secrets.AZURE_TENANT_ID }} - az account set --subscription ${{ secrets.AZURE_SUBSCRIPTION_ID }} - - name: Check and Create Resource Group id: check_create_rg run: | @@ -252,11 +254,8 @@ jobs: Write-Host "Resource Group Name is ${{ env.RESOURCE_GROUP_NAME }}" Write-Host "Kubernetes resource group is ${{ env.AZURE_AKS_NAME }}" env: - # From GitHub secrets (for login) + # From GitHub secrets AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} - AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} - AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} # From deployment outputs step (these come from $GITHUB_ENV) RESOURCE_GROUP_NAME: ${{ env.RESOURCE_GROUP_NAME }} @@ -292,10 +291,9 @@ jobs: if az account show &> /dev/null; then echo "Azure CLI is authenticated." else - echo "Azure CLI is not authenticated. Logging in..." - az login --service-principal --username ${{ secrets.AZURE_CLIENT_ID }} --password ${{ secrets.AZURE_CLIENT_SECRET }} --tenant ${{ secrets.AZURE_TENANT_ID }} + echo "Azure CLI is not authenticated. Please check the OIDC login step." + exit 1 fi - az account set --subscription ${{ secrets.AZURE_SUBSCRIPTION_ID }} # Get the Web App URL and save it to GITHUB_OUTPUT echo "Retrieving Web App URL..." @@ -393,6 +391,7 @@ jobs: if: always() needs: [deploy, e2e-test] runs-on: ubuntu-latest + environment: production env: RESOURCE_GROUP_NAME: ${{ needs.deploy.outputs.RESOURCE_GROUP_NAME }} KUBERNETES_RESOURCE_GROUP_NAME: ${{ needs.deploy.outputs.KUBERNETES_RESOURCE_GROUP_NAME }} @@ -402,10 +401,11 @@ jobs: steps: - name: Login to Azure - shell: bash - run: | - az login --service-principal --username ${{ secrets.AZURE_CLIENT_ID }} --password ${{ secrets.AZURE_CLIENT_SECRET }} --tenant ${{ secrets.AZURE_TENANT_ID }} - az account set --subscription "${{ secrets.AZURE_SUBSCRIPTION_ID }}" + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - name: Delete Resource Groups if: env.RESOURCE_GROUP_NAME != '' diff --git a/.github/workflows/deploy-orchestrator.yml b/.github/workflows/deploy-orchestrator.yml index 1b8e1cbd..aa2cdfc2 100644 --- a/.github/workflows/deploy-orchestrator.yml +++ b/.github/workflows/deploy-orchestrator.yml @@ -77,7 +77,8 @@ jobs: secrets: inherit send-notification: - if: "!cancelled()" + # if: "!cancelled()" + if: false # Temporarily disable notification job needs: [deploy, e2e-test] uses: ./.github/workflows/job-send-notification.yml with: diff --git a/.github/workflows/deploy-linux.yml b/.github/workflows/deploy-v2.yml similarity index 99% rename from .github/workflows/deploy-linux.yml rename to .github/workflows/deploy-v2.yml index f31ed3fc..e52e6d7e 100644 --- a/.github/workflows/deploy-linux.yml +++ b/.github/workflows/deploy-v2.yml @@ -68,6 +68,11 @@ on: default: '' type: string +permissions: + id-token: write + contents: read + actions: read + jobs: validate-inputs: name: Validate Input Parameters diff --git a/.github/workflows/job-cleanup-deployment.yml b/.github/workflows/job-cleanup-deployment.yml index 428e4da5..082dffe8 100644 --- a/.github/workflows/job-cleanup-deployment.yml +++ b/.github/workflows/job-cleanup-deployment.yml @@ -40,6 +40,7 @@ on: jobs: cleanup-deployment: runs-on: ubuntu-latest + environment: production continue-on-error: true env: RESOURCE_GROUP_NAME: ${{ inputs.RESOURCE_GROUP_NAME }} @@ -150,10 +151,11 @@ jobs: az --version - name: Login to Azure - shell: bash - run: | - az login --service-principal --username ${{ secrets.AZURE_CLIENT_ID }} --password ${{ secrets.AZURE_CLIENT_SECRET }} --tenant ${{ secrets.AZURE_TENANT_ID }} - az account set --subscription ${{ secrets.AZURE_SUBSCRIPTION_ID }} + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - name: Delete Resource Group (Optimized Cleanup) id: delete_rg diff --git a/.github/workflows/job-deploy-linux.yml b/.github/workflows/job-deploy-linux.yml index 28708215..ca9488ab 100644 --- a/.github/workflows/job-deploy-linux.yml +++ b/.github/workflows/job-deploy-linux.yml @@ -36,6 +36,7 @@ on: jobs: deploy-linux: runs-on: ubuntu-latest + environment: production env: AZURE_DEV_COLLECT_TELEMETRY: ${{ vars.AZURE_DEV_COLLECT_TELEMETRY }} outputs: @@ -221,10 +222,15 @@ jobs: uses: Azure/setup-azd@v2 - name: Login to Azure + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Login to azd run: | - az login --service-principal --username ${{ secrets.AZURE_CLIENT_ID }} --password ${{ secrets.AZURE_CLIENT_SECRET }} --tenant ${{ secrets.AZURE_TENANT_ID }} - az account set --subscription ${{ secrets.AZURE_SUBSCRIPTION_ID }} - azd auth login --client-id ${{ secrets.AZURE_CLIENT_ID }} --client-secret ${{ secrets.AZURE_CLIENT_SECRET }} --tenant-id ${{ secrets.AZURE_TENANT_ID }} + azd auth login --client-id ${{ secrets.AZURE_CLIENT_ID }} --federated-credential-provider "github" --tenant-id ${{ secrets.AZURE_TENANT_ID }} - name: Deploy using azd up @@ -299,15 +305,20 @@ jobs: echo "AKS node resource group: $krg_name" fi fi + + - name: Login to Azure to refresh credentials for subsequent steps + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + enable-AzPSSession: true - name: Run Deployment Script with Input shell: pwsh env: - # From GitHub secrets (for login) + # From GitHub secrets AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} - AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} - AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} # From workflow inputs and deployment outputs RESOURCE_GROUP_NAME: ${{ inputs.RESOURCE_GROUP_NAME }} diff --git a/.github/workflows/job-deploy.yml b/.github/workflows/job-deploy.yml index 99ff9717..d1ff4009 100644 --- a/.github/workflows/job-deploy.yml +++ b/.github/workflows/job-deploy.yml @@ -85,6 +85,7 @@ jobs: name: Azure Setup if: inputs.trigger_type != 'workflow_dispatch' || inputs.existing_webapp_url == '' || inputs.existing_webapp_url == null runs-on: ubuntu-latest + environment: production outputs: RESOURCE_GROUP_NAME: ${{ steps.check_create_rg.outputs.RESOURCE_GROUP_NAME }} ENV_NAME: ${{ steps.generate_env_name.outputs.ENV_NAME }} @@ -244,10 +245,12 @@ jobs: uses: actions/checkout@v4 - name: Login to Azure - shell: bash - run: | - az login --service-principal --username ${{ secrets.AZURE_CLIENT_ID }} --password ${{ secrets.AZURE_CLIENT_SECRET }} --tenant ${{ secrets.AZURE_TENANT_ID }} - az account set --subscription ${{ secrets.AZURE_SUBSCRIPTION_ID }} + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + enable-AzPSSession: true - name: Run Quota Check id: quota-check @@ -275,9 +278,6 @@ jobs: } env: AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} - AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} - AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} GPT_MIN_CAPACITY: ${{ env.GPT_MIN_CAPACITY }} TEXT_EMBEDDING_MIN_CAPACITY: ${{ env.TEXT_EMBEDDING_MIN_CAPACITY }} AZURE_REGIONS: "${{ vars.AZURE_REGIONS }}" diff --git a/.github/workflows/test-automation-v2.yml b/.github/workflows/test-automation-v2.yml index fe64a1ec..20e8e48a 100644 --- a/.github/workflows/test-automation-v2.yml +++ b/.github/workflows/test-automation-v2.yml @@ -28,6 +28,7 @@ env: jobs: test: runs-on: ubuntu-latest + environment: production outputs: TEST_SUCCESS: ${{ steps.test1.outcome == 'success' || steps.test2.outcome == 'success' || steps.test3.outcome == 'success' }} TEST_REPORT_URL: ${{ steps.upload_report.outputs.artifact-url }} @@ -41,13 +42,11 @@ jobs: python-version: '3.13' - name: Login to Azure - env: - AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} - AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} - AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} - run: | - az login --service-principal --username "$AZURE_CLIENT_ID" --password "$AZURE_CLIENT_SECRET" --tenant "$AZURE_TENANT_ID" - az account set --subscription ${{ secrets.AZURE_SUBSCRIPTION_ID }} + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - name: Install dependencies run: | diff --git a/.github/workflows/test-automation.yml b/.github/workflows/test-automation.yml index dd6e5a57..aa0ae606 100644 --- a/.github/workflows/test-automation.yml +++ b/.github/workflows/test-automation.yml @@ -15,9 +15,6 @@ on: env: url: ${{ inputs.DKM_URL }} accelerator_name: "DKM" -permissions: - contents: read - actions: read jobs: test: runs-on: ubuntu-latest diff --git a/Deployment/checkquota.ps1 b/Deployment/checkquota.ps1 index cc5c4822..c16a0b85 100644 --- a/Deployment/checkquota.ps1 +++ b/Deployment/checkquota.ps1 @@ -8,24 +8,20 @@ Write-Output "📍 Processed Regions: $($REGIONS -join ', ')" $SUBSCRIPTION_ID = $env:AZURE_SUBSCRIPTION_ID $GPT_MIN_CAPACITY = $env:GPT_MIN_CAPACITY $TEXT_EMBEDDING_MIN_CAPACITY = $env:TEXT_EMBEDDING_MIN_CAPACITY -$AZURE_CLIENT_ID = $env:AZURE_CLIENT_ID -$AZURE_TENANT_ID = $env:AZURE_TENANT_ID -$AZURE_CLIENT_SECRET = $env:AZURE_CLIENT_SECRET - -# Authenticate using Service Principal -Write-Host "Authentication using Service Principal..." # Ensure Azure PowerShell module is installed and imported Install-Module -Name Az -AllowClobber -Force -Scope CurrentUser Import-Module Az -# Create a PSCredential object for authentication -$creds = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $AZURE_CLIENT_ID, (ConvertTo-SecureString $AZURE_CLIENT_SECRET -AsPlainText -Force) - -# Attempt to connect using Service Principal +# Verify existing Azure session (authentication is handled by the caller workflow via OIDC) try { - Connect-AzAccount -ServicePrincipal -TenantId $AZURE_TENANT_ID -Credential $creds + $context = Get-AzContext + if (-not $context) { + Write-Host "❌ Error: No active Azure session found. Ensure the caller workflow authenticates via azure/login@v2 with enable-AzPSSession: true." + exit 1 + } + Write-Host "✅ Using existing Azure session: $($context.Account.Id)" } catch { - Write-Host "❌ Error: Failed to authenticate using Service Principal. $_" + Write-Host "❌ Error: Failed to verify Azure session. $_" exit 1 } diff --git a/Deployment/resourcedeployment.ps1 b/Deployment/resourcedeployment.ps1 index 9919ba9a..9e4f4dce 100644 --- a/Deployment/resourcedeployment.ps1 +++ b/Deployment/resourcedeployment.ps1 @@ -120,11 +120,14 @@ function LoginAzure([string]$tenantId, [string]$subscriptionID) { } } if ($env:CI -eq "true"){ - az login --service-principal ` - --username $env:AZURE_CLIENT_ID ` - --password $env:AZURE_CLIENT_SECRET ` - --tenant $env:AZURE_TENANT_ID ` - Write-Host "CI deployment mode" + # Authentication is handled by the caller workflow via OIDC (azure/login@v2) + $account = az account show 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Host "❌ Error: No active Azure CLI session found. Ensure the caller workflow authenticates via azure/login@v2." -ForegroundColor Red + failureBanner + exit 1 + } + Write-Host "CI deployment mode - using existing OIDC session" } else{ az login --tenant $tenantId From c823a25a0b626d96dc3979d777778e2a2d6391e1 Mon Sep 17 00:00:00 2001 From: Vamshi-Microsoft Date: Mon, 2 Mar 2026 11:52:45 +0530 Subject: [PATCH 47/49] Remove paths-ignore entries from CodeQL workflow --- .github/workflows/codeql.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 7a7342cb..d56a9fb2 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -8,10 +8,6 @@ on: - 'App/frontend-app/**' - 'App/kernel-memory/**' - '.github/workflows/codeql.yml' - paths-ignore: - - '**/.gitignore' - - '**/Dockerfile' - - '**/.dockerignore' pull_request: branches: [ "main", "dev", "demo" ] paths: @@ -19,10 +15,6 @@ on: - 'App/frontend-app/**' - 'App/kernel-memory/**' - '.github/workflows/codeql.yml' - paths-ignore: - - '**/.gitignore' - - '**/Dockerfile' - - '**/.dockerignore' schedule: - cron: '37 2 * * 5' From e34ac5628dfa30d9e21c74a183d362885fd34201 Mon Sep 17 00:00:00 2001 From: Vamshi-Microsoft Date: Mon, 2 Mar 2026 11:53:42 +0530 Subject: [PATCH 48/49] renamed to deploy-linux for testing --- .github/workflows/{deploy-v2.yml => deploy-linux.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{deploy-v2.yml => deploy-linux.yml} (100%) diff --git a/.github/workflows/deploy-v2.yml b/.github/workflows/deploy-linux.yml similarity index 100% rename from .github/workflows/deploy-v2.yml rename to .github/workflows/deploy-linux.yml From db3b477dc5354db6dcd4a83c93798ba8c4f70ddd Mon Sep 17 00:00:00 2001 From: Vamshi-Microsoft Date: Wed, 4 Mar 2026 10:34:53 +0530 Subject: [PATCH 49/49] Minor updates --- .github/workflows/{deploy-linux.yml => deploy-v2.yml} | 0 .github/workflows/job-deploy.yml | 2 -- 2 files changed, 2 deletions(-) rename .github/workflows/{deploy-linux.yml => deploy-v2.yml} (100%) diff --git a/.github/workflows/deploy-linux.yml b/.github/workflows/deploy-v2.yml similarity index 100% rename from .github/workflows/deploy-linux.yml rename to .github/workflows/deploy-v2.yml diff --git a/.github/workflows/job-deploy.yml b/.github/workflows/job-deploy.yml index d1ff4009..1464b151 100644 --- a/.github/workflows/job-deploy.yml +++ b/.github/workflows/job-deploy.yml @@ -417,8 +417,6 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY echo "| Configuration | Value |" >> $GITHUB_STEP_SUMMARY echo "|---------------|-------|" >> $GITHUB_STEP_SUMMARY - echo "| **Trigger Type** | \`${{ github.event_name }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| **Branch** | \`${{ env.BRANCH_NAME }}\` |" >> $GITHUB_STEP_SUMMARY echo "| **WAF Enabled** | ${{ env.WAF_ENABLED == 'true' && '✅ Yes' || '❌ No' }} |" >> $GITHUB_STEP_SUMMARY echo "| **EXP Enabled** | ${{ env.EXP == 'true' && '✅ Yes' || '❌ No' }} |" >> $GITHUB_STEP_SUMMARY echo "| **Run E2E Tests** | \`${{ env.RUN_E2E_TESTS }}\` |" >> $GITHUB_STEP_SUMMARY