From 66e84abd059fd5908c22187047c5c472d4a2b44f Mon Sep 17 00:00:00 2001 From: "Ashwal Vishwanath (Persistent Systems Inc)" Date: Fri, 12 Jun 2026 17:18:05 +0530 Subject: [PATCH 01/14] fix: add swedencentral to azureAiServiceLocation allowed list and add dependsOn to cognitive service private endpoint --- infra/main.bicep | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/infra/main.bicep b/infra/main.bicep index c29e258f..2c9292fd 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -42,6 +42,7 @@ param contentUnderstandingLocation string = 'WestUS' 'japaneast' 'northeurope' 'southeastasia' + 'swedencentral' 'uksouth' ]) @description('Required. Location for the Azure AI Services deployment.') @@ -827,6 +828,11 @@ module cognitiveServicePrivateEndpoint 'br/public:avm/res/network/private-endpoi } subnetResourceId: virtualNetwork!.outputs.backendSubnetResourceId } + dependsOn: [ + avmAiServices + virtualNetwork + avmPrivateDnsZones + ] } module avmAiServices_cu 'br/public:avm/res/cognitive-services/account:0.14.1' = { From 8ee49159c753229042eed5d1e4bcc4fc514a8987 Mon Sep 17 00:00:00 2001 From: "Ashwal Vishwanath (Persistent Systems Inc)" Date: Fri, 12 Jun 2026 17:39:03 +0530 Subject: [PATCH 02/14] fix: auto-select available GPT quota capacity and pass to Bicep deployment --- .github/workflows/job-deploy-linux.yml | 7 +++++++ .github/workflows/job-deploy-windows.yml | 8 ++++++++ .github/workflows/job-deploy.yml | 7 +++++++ infra/main.bicep | 4 ++-- infra/scripts/checkquota.sh | 3 +++ 5 files changed, 27 insertions(+), 2 deletions(-) diff --git a/.github/workflows/job-deploy-linux.yml b/.github/workflows/job-deploy-linux.yml index 002baa7b..559a9bc9 100644 --- a/.github/workflows/job-deploy-linux.yml +++ b/.github/workflows/job-deploy-linux.yml @@ -34,6 +34,10 @@ on: AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: required: false type: string + AZURE_ENV_GPT_MODEL_CAPACITY: + required: false + type: string + default: "100" outputs: CONTAINER_WEB_APPURL: description: "Container Web App URL" @@ -45,6 +49,7 @@ jobs: environment: production env: AZURE_DEV_COLLECT_TELEMETRY: ${{ vars.AZURE_DEV_COLLECT_TELEMETRY }} + AZURE_ENV_GPT_MODEL_CAPACITY: ${{ inputs.AZURE_ENV_GPT_MODEL_CAPACITY || '100' }} outputs: CONTAINER_WEB_APPURL: ${{ steps.get_output_linux.outputs.CONTAINER_WEB_APPURL }} steps: @@ -247,6 +252,8 @@ jobs: azd env set AZURE_LOCATION="$AZURE_LOCATION" azd env set AZURE_RESOURCE_GROUP="$RESOURCE_GROUP_NAME" azd env set AZURE_ENV_IMAGETAG="$IMAGE_TAG" + # Use available quota capacity discovered by checkquota.sh; fall back to 100 if not set + azd env set AZURE_ENV_GPT_MODEL_CAPACITY="${AZURE_ENV_GPT_MODEL_CAPACITY:-100}" if [[ "$BUILD_DOCKER_IMAGE" == "true" ]]; then ACR_NAME=$(echo "${{ secrets.ACR_TEST_LOGIN_SERVER }}") diff --git a/.github/workflows/job-deploy-windows.yml b/.github/workflows/job-deploy-windows.yml index c33b8c01..c200e536 100644 --- a/.github/workflows/job-deploy-windows.yml +++ b/.github/workflows/job-deploy-windows.yml @@ -34,6 +34,10 @@ on: AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: required: false type: string + AZURE_ENV_GPT_MODEL_CAPACITY: + required: false + type: string + default: "100" outputs: CONTAINER_WEB_APPURL: description: "Container Web App URL" @@ -45,6 +49,7 @@ jobs: environment: production env: AZURE_DEV_COLLECT_TELEMETRY: ${{ vars.AZURE_DEV_COLLECT_TELEMETRY }} + AZURE_ENV_GPT_MODEL_CAPACITY: ${{ inputs.AZURE_ENV_GPT_MODEL_CAPACITY || '100' }} outputs: CONTAINER_WEB_APPURL: ${{ steps.get_output_windows.outputs.CONTAINER_WEB_APPURL }} steps: @@ -244,6 +249,9 @@ jobs: azd env set AZURE_LOCATION="$env:AZURE_LOCATION" azd env set AZURE_RESOURCE_GROUP="$env:RESOURCE_GROUP_NAME" azd env set AZURE_ENV_IMAGETAG="$env:IMAGE_TAG" + # Use available quota capacity discovered by checkquota.sh; fall back to 100 if not set + $gptCapacity = if ($env:AZURE_ENV_GPT_MODEL_CAPACITY) { $env:AZURE_ENV_GPT_MODEL_CAPACITY } else { "100" } + azd env set AZURE_ENV_GPT_MODEL_CAPACITY="$gptCapacity" # Set ACR name only when building Docker image if ($env:BUILD_DOCKER_IMAGE -eq "true") { diff --git a/.github/workflows/job-deploy.yml b/.github/workflows/job-deploy.yml index 8459acea..f58be0d8 100644 --- a/.github/workflows/job-deploy.yml +++ b/.github/workflows/job-deploy.yml @@ -110,6 +110,7 @@ jobs: 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 }} + AZURE_ENV_GPT_MODEL_CAPACITY: ${{ steps.set_region.outputs.AZURE_ENV_GPT_MODEL_CAPACITY }} 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 }} @@ -359,8 +360,12 @@ jobs: INPUT_AZURE_LOCATION: ${{ inputs.azure_location }} run: | echo "Selected Region from Quota Check: $VALID_REGION" + echo "Available Capacity in $VALID_REGION: $AVAILABLE_CAPACITY" echo "AZURE_ENV_OPENAI_LOCATION=$VALID_REGION" >> $GITHUB_ENV echo "AZURE_ENV_OPENAI_LOCATION=$VALID_REGION" >> $GITHUB_OUTPUT + # Pass the actual available capacity so Bicep deploys only what is free + echo "AZURE_ENV_GPT_MODEL_CAPACITY=$AVAILABLE_CAPACITY" >> $GITHUB_ENV + echo "AZURE_ENV_GPT_MODEL_CAPACITY=$AVAILABLE_CAPACITY" >> $GITHUB_OUTPUT if [[ "$INPUT_TRIGGER_TYPE" == "workflow_dispatch" && -n "$INPUT_AZURE_LOCATION" ]]; then USER_SELECTED_LOCATION="$INPUT_AZURE_LOCATION" @@ -533,6 +538,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 }} + AZURE_ENV_GPT_MODEL_CAPACITY: ${{ needs.azure-setup.outputs.AZURE_ENV_GPT_MODEL_CAPACITY }} BUILD_DOCKER_IMAGE: ${{ inputs.build_docker_image || 'false' }} EXP: ${{ needs.azure-setup.outputs.EXP_ENABLED }} WAF_ENABLED: ${{ inputs.waf_enabled == true && 'true' || 'false' }} @@ -551,6 +557,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 }} + AZURE_ENV_GPT_MODEL_CAPACITY: ${{ needs.azure-setup.outputs.AZURE_ENV_GPT_MODEL_CAPACITY }} BUILD_DOCKER_IMAGE: ${{ inputs.build_docker_image || 'false' }} EXP: ${{ needs.azure-setup.outputs.EXP_ENABLED }} WAF_ENABLED: ${{ inputs.waf_enabled == true && 'true' || 'false' }} diff --git a/infra/main.bicep b/infra/main.bicep index 2c9292fd..72a3760e 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -50,7 +50,7 @@ param contentUnderstandingLocation string = 'WestUS' azd: { type: 'location' usageName: [ - 'OpenAI.GlobalStandard.gpt-5.1,300' + 'OpenAI.GlobalStandard.gpt-5.1,100' ] } }) @@ -76,7 +76,7 @@ param gptModelVersion string = '2025-11-13' @minValue(1) @description('Optional. Capacity of the GPT deployment: (minimum 10).') -param gptDeploymentCapacity int = 300 +param gptDeploymentCapacity int = 100 @description('Optional. The container registry login server/endpoint for the container images (for example, an Azure Container Registry endpoint).') param containerRegistryEndpoint string = 'cpscontainerreg.azurecr.io' diff --git a/infra/scripts/checkquota.sh b/infra/scripts/checkquota.sh index a85b0db9..d5a2f658 100644 --- a/infra/scripts/checkquota.sh +++ b/infra/scripts/checkquota.sh @@ -77,6 +77,7 @@ for REGION in "${REGIONS[@]}"; do if [ "$INSUFFICIENT_QUOTA" = false ]; then VALID_REGION="$REGION" + VALID_REGION_AVAILABLE_CAPACITY=$AVAILABLE break fi @@ -88,6 +89,8 @@ if [ -z "$VALID_REGION" ]; then exit 0 else echo "✅ Suggested Region: $VALID_REGION" + echo "✅ Available Capacity: $VALID_REGION_AVAILABLE_CAPACITY" echo "VALID_REGION=$VALID_REGION" >> "$GITHUB_ENV" + echo "AVAILABLE_CAPACITY=$VALID_REGION_AVAILABLE_CAPACITY" >> "$GITHUB_ENV" exit 0 fi From 7f225de8eae97e1791bfa43be300451ed919985d Mon Sep 17 00:00:00 2001 From: "Ashwal Vishwanath (Persistent Systems Inc)" Date: Mon, 15 Jun 2026 11:07:23 +0530 Subject: [PATCH 03/14] fix: validate AZURE_REGIONS against allowed Bicep regions (eastus2 not eastus) --- infra/scripts/checkquota.sh | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/infra/scripts/checkquota.sh b/infra/scripts/checkquota.sh index d5a2f658..194f2282 100644 --- a/infra/scripts/checkquota.sh +++ b/infra/scripts/checkquota.sh @@ -1,7 +1,30 @@ #!/bin/bash -# List of Azure regions to check for quota (update as needed) -IFS=', ' read -ra REGIONS <<< "$AZURE_REGIONS" +# List of valid Azure regions for AI Services (must match Bicep @allowed values) +# These are the only regions where GPT-5.1 GlobalStandard is available +ALLOWED_REGIONS=("australiaeast" "centralus" "eastasia" "eastus2" "japaneast" "northeurope" "southeastasia" "swedencentral" "uksouth") + +# Get requested regions from environment variable, default to all allowed regions +if [[ -n "$AZURE_REGIONS" ]]; then + IFS=',' read -ra REQUESTED_REGIONS <<< "$AZURE_REGIONS" + # Filter requested regions to only include those in ALLOWED_REGIONS + REGIONS=() + for req_region in "${REQUESTED_REGIONS[@]}"; do + req_region=$(echo "$req_region" | xargs) # trim whitespace + for allowed in "${ALLOWED_REGIONS[@]}"; do + if [[ "$req_region" == "$allowed" ]]; then + REGIONS+=("$req_region") + break + fi + done + done + if [[ ${#REGIONS[@]} -eq 0 ]]; then + echo "⚠️ WARNING: No valid regions found in AZURE_REGIONS. Using all allowed regions." + REGIONS=("${ALLOWED_REGIONS[@]}") + fi +else + REGIONS=("${ALLOWED_REGIONS[@]}") +fi SUBSCRIPTION_ID="${AZURE_SUBSCRIPTION_ID}" GPT_MIN_CAPACITY="${GPT_MIN_CAPACITY}" From 3a2ae42721fd4f8600c3168cb50a12b722511ef7 Mon Sep 17 00:00:00 2001 From: "Ashwal Vishwanath (Persistent Systems Inc)" Date: Mon, 15 Jun 2026 15:13:47 +0530 Subject: [PATCH 04/14] revert: restore GPT default capacity to 300 --- infra/main.bicep | 4 ++-- infra/main.json | 17 +++++++++++------ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/infra/main.bicep b/infra/main.bicep index 72a3760e..2c9292fd 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -50,7 +50,7 @@ param contentUnderstandingLocation string = 'WestUS' azd: { type: 'location' usageName: [ - 'OpenAI.GlobalStandard.gpt-5.1,100' + 'OpenAI.GlobalStandard.gpt-5.1,300' ] } }) @@ -76,7 +76,7 @@ param gptModelVersion string = '2025-11-13' @minValue(1) @description('Optional. Capacity of the GPT deployment: (minimum 10).') -param gptDeploymentCapacity int = 100 +param gptDeploymentCapacity int = 300 @description('Optional. The container registry login server/endpoint for the container images (for example, an Azure Container Registry endpoint).') param containerRegistryEndpoint string = 'cpscontainerreg.azurecr.io' diff --git a/infra/main.json b/infra/main.json index 4280795f..2cf7180c 100644 --- a/infra/main.json +++ b/infra/main.json @@ -6,7 +6,7 @@ "_generator": { "name": "bicep", "version": "0.41.2.15936", - "templateHash": "5358772599129171911" + "templateHash": "2597731928932727268" }, "name": "Content Processing Solution Accelerator", "description": "Bicep template to deploy the Content Processing Solution Accelerator with AVM compliance." @@ -66,6 +66,7 @@ "japaneast", "northeurope", "southeastasia", + "swedencentral", "uksouth" ], "metadata": { @@ -35187,8 +35188,8 @@ "avmContainerApp_API", "avmContainerApp_Workflow", "avmManagedIdentity", - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').storageQueue)]", "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').storageBlob)]", + "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').storageQueue)]", "virtualNetwork" ] }, @@ -41721,10 +41722,7 @@ }, "dependsOn": [ "avmAiServices", - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').contentUnderstanding)]", - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').cognitiveServices)]", - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').aiServices)]", - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').openAI)]", + "avmPrivateDnsZones", "virtualNetwork" ] }, @@ -68558,6 +68556,13 @@ }, "value": "[reference('avmContainerRegistry').outputs.loginServer.value]" }, + "CONTENT_UNDERSTANDING_ACCOUNT_NAME": { + "type": "string", + "metadata": { + "description": "The name of the Content Understanding AI Services account." + }, + "value": "[reference('avmAiServices_cu').outputs.name.value]" + }, "AZURE_RESOURCE_GROUP": { "type": "string", "metadata": { From bad3e10a7117ec1fcacdfa3b9ced66959a071454 Mon Sep 17 00:00:00 2001 From: "Ashwal Vishwanath (Persistent Systems Inc)" Date: Mon, 15 Jun 2026 15:18:19 +0530 Subject: [PATCH 05/14] chore: align workflow GPT quota defaults to 300 --- .github/workflows/deploy.yml | 2 +- .github/workflows/job-deploy-linux.yml | 8 ++++---- .github/workflows/job-deploy-windows.yml | 8 ++++---- .github/workflows/job-deploy.yml | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 92b76912..5f1feb28 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -46,7 +46,7 @@ jobs: id: quota-check env: AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - GPT_MIN_CAPACITY: "100" + GPT_MIN_CAPACITY: "300" AZURE_REGIONS: ${{ vars.AZURE_REGIONS }} run: | chmod +x infra/scripts/checkquota.sh diff --git a/.github/workflows/job-deploy-linux.yml b/.github/workflows/job-deploy-linux.yml index 559a9bc9..7b58ab1e 100644 --- a/.github/workflows/job-deploy-linux.yml +++ b/.github/workflows/job-deploy-linux.yml @@ -37,7 +37,7 @@ on: AZURE_ENV_GPT_MODEL_CAPACITY: required: false type: string - default: "100" + default: "300" outputs: CONTAINER_WEB_APPURL: description: "Container Web App URL" @@ -49,7 +49,7 @@ jobs: environment: production env: AZURE_DEV_COLLECT_TELEMETRY: ${{ vars.AZURE_DEV_COLLECT_TELEMETRY }} - AZURE_ENV_GPT_MODEL_CAPACITY: ${{ inputs.AZURE_ENV_GPT_MODEL_CAPACITY || '100' }} + AZURE_ENV_GPT_MODEL_CAPACITY: ${{ inputs.AZURE_ENV_GPT_MODEL_CAPACITY || '300' }} outputs: CONTAINER_WEB_APPURL: ${{ steps.get_output_linux.outputs.CONTAINER_WEB_APPURL }} steps: @@ -252,8 +252,8 @@ jobs: azd env set AZURE_LOCATION="$AZURE_LOCATION" azd env set AZURE_RESOURCE_GROUP="$RESOURCE_GROUP_NAME" azd env set AZURE_ENV_IMAGETAG="$IMAGE_TAG" - # Use available quota capacity discovered by checkquota.sh; fall back to 100 if not set - azd env set AZURE_ENV_GPT_MODEL_CAPACITY="${AZURE_ENV_GPT_MODEL_CAPACITY:-100}" + # Use available quota capacity discovered by checkquota.sh; fall back to 300 if not set + azd env set AZURE_ENV_GPT_MODEL_CAPACITY="${AZURE_ENV_GPT_MODEL_CAPACITY:-300}" if [[ "$BUILD_DOCKER_IMAGE" == "true" ]]; then ACR_NAME=$(echo "${{ secrets.ACR_TEST_LOGIN_SERVER }}") diff --git a/.github/workflows/job-deploy-windows.yml b/.github/workflows/job-deploy-windows.yml index c200e536..33630cca 100644 --- a/.github/workflows/job-deploy-windows.yml +++ b/.github/workflows/job-deploy-windows.yml @@ -37,7 +37,7 @@ on: AZURE_ENV_GPT_MODEL_CAPACITY: required: false type: string - default: "100" + default: "300" outputs: CONTAINER_WEB_APPURL: description: "Container Web App URL" @@ -49,7 +49,7 @@ jobs: environment: production env: AZURE_DEV_COLLECT_TELEMETRY: ${{ vars.AZURE_DEV_COLLECT_TELEMETRY }} - AZURE_ENV_GPT_MODEL_CAPACITY: ${{ inputs.AZURE_ENV_GPT_MODEL_CAPACITY || '100' }} + AZURE_ENV_GPT_MODEL_CAPACITY: ${{ inputs.AZURE_ENV_GPT_MODEL_CAPACITY || '300' }} outputs: CONTAINER_WEB_APPURL: ${{ steps.get_output_windows.outputs.CONTAINER_WEB_APPURL }} steps: @@ -249,8 +249,8 @@ jobs: azd env set AZURE_LOCATION="$env:AZURE_LOCATION" azd env set AZURE_RESOURCE_GROUP="$env:RESOURCE_GROUP_NAME" azd env set AZURE_ENV_IMAGETAG="$env:IMAGE_TAG" - # Use available quota capacity discovered by checkquota.sh; fall back to 100 if not set - $gptCapacity = if ($env:AZURE_ENV_GPT_MODEL_CAPACITY) { $env:AZURE_ENV_GPT_MODEL_CAPACITY } else { "100" } + # Use available quota capacity discovered by checkquota.sh; fall back to 300 if not set + $gptCapacity = if ($env:AZURE_ENV_GPT_MODEL_CAPACITY) { $env:AZURE_ENV_GPT_MODEL_CAPACITY } else { "300" } azd env set AZURE_ENV_GPT_MODEL_CAPACITY="$gptCapacity" # Set ACR name only when building Docker image diff --git a/.github/workflows/job-deploy.yml b/.github/workflows/job-deploy.yml index f58be0d8..2eb08b44 100644 --- a/.github/workflows/job-deploy.yml +++ b/.github/workflows/job-deploy.yml @@ -90,7 +90,7 @@ on: value: ${{ jobs.azure-setup.outputs.QUOTA_FAILED }} env: - GPT_MIN_CAPACITY: 100 + GPT_MIN_CAPACITY: 300 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 }} From a9c9552441b0c3d0c032cc4c4af218ddecc38548 Mon Sep 17 00:00:00 2001 From: "Ashwal Vishwanath (Persistent Systems Inc)" Date: Mon, 15 Jun 2026 17:39:37 +0530 Subject: [PATCH 06/14] fix: handle missing Cognitive Services account gracefully in post-deployment script --- infra/scripts/post_deployment.sh | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/infra/scripts/post_deployment.sh b/infra/scripts/post_deployment.sh index 2b0ee0ad..82bad847 100644 --- a/infra/scripts/post_deployment.sh +++ b/infra/scripts/post_deployment.sh @@ -250,14 +250,20 @@ CU_ACCOUNT_NAME=$(azd env get-value CONTENT_UNDERSTANDING_ACCOUNT_NAME 2>/dev/nu if [ -z "$CU_ACCOUNT_NAME" ]; then echo " ⚠️ CONTENT_UNDERSTANDING_ACCOUNT_NAME not found in azd env. Skipping refresh." else - echo " Refreshing account: $CU_ACCOUNT_NAME in resource group: $RESOURCE_GROUP" - if az cognitiveservices account update \ + echo " Checking account: $CU_ACCOUNT_NAME in resource group: $RESOURCE_GROUP" + + # Check if the resource group exists first + if ! az group exists -n "$RESOURCE_GROUP" --output none 2>/dev/null; then + echo " ⚠️ Resource group '$RESOURCE_GROUP' does not exist yet. Skipping refresh (expected on fresh deployment)." + elif ! az cognitiveservices account show -g "$RESOURCE_GROUP" -n "$CU_ACCOUNT_NAME" --output none 2>/dev/null; then + echo " ⚠️ Cognitive Services account '$CU_ACCOUNT_NAME' not found. Skipping refresh (expected on fresh deployment)." + elif az cognitiveservices account update \ -g "$RESOURCE_GROUP" \ -n "$CU_ACCOUNT_NAME" \ --tags refresh=true \ - --output none; then + --output none 2>/dev/null; then echo " ✅ Successfully refreshed Cognitive Services account '$CU_ACCOUNT_NAME'." else - echo " ❌ Failed to refresh Cognitive Services account '$CU_ACCOUNT_NAME'." + echo " ⚠️ Failed to refresh Cognitive Services account '$CU_ACCOUNT_NAME' (non-critical)." fi fi From 6b27a98fcfc4552e6c0cebaf8392cf44ef705d08 Mon Sep 17 00:00:00 2001 From: "Ashwal Vishwanath (Persistent Systems Inc)" Date: Mon, 15 Jun 2026 23:19:12 +0530 Subject: [PATCH 07/14] chore: reduce GPT default capacity from 300 to 100 across all workflows --- .github/workflows/deploy.yml | 4 ++-- .github/workflows/job-deploy-linux.yml | 8 ++++---- .github/workflows/job-deploy-windows.yml | 8 ++++---- .github/workflows/job-deploy.yml | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 5f1feb28..80fa701d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -46,7 +46,7 @@ jobs: id: quota-check env: AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - GPT_MIN_CAPACITY: "300" + GPT_MIN_CAPACITY: "100" AZURE_REGIONS: ${{ vars.AZURE_REGIONS }} run: | chmod +x infra/scripts/checkquota.sh @@ -150,7 +150,7 @@ jobs: deploymentType="GlobalStandard" \ gptModelName="gpt-5.1" \ gptModelVersion="2025-11-13" \ - gptDeploymentCapacity="300" \ + gptDeploymentCapacity="100" \ azureAiServiceLocation="${{ env.AZURE_LOCATION }}" \ imageTag="latest_v2" \ tags="{'CreatedBy':'Pipeline', 'Purpose':'Deploying and Cleaning Up Resources for Validation','CreatedDate':'$current_date'}" \ diff --git a/.github/workflows/job-deploy-linux.yml b/.github/workflows/job-deploy-linux.yml index 7b58ab1e..559a9bc9 100644 --- a/.github/workflows/job-deploy-linux.yml +++ b/.github/workflows/job-deploy-linux.yml @@ -37,7 +37,7 @@ on: AZURE_ENV_GPT_MODEL_CAPACITY: required: false type: string - default: "300" + default: "100" outputs: CONTAINER_WEB_APPURL: description: "Container Web App URL" @@ -49,7 +49,7 @@ jobs: environment: production env: AZURE_DEV_COLLECT_TELEMETRY: ${{ vars.AZURE_DEV_COLLECT_TELEMETRY }} - AZURE_ENV_GPT_MODEL_CAPACITY: ${{ inputs.AZURE_ENV_GPT_MODEL_CAPACITY || '300' }} + AZURE_ENV_GPT_MODEL_CAPACITY: ${{ inputs.AZURE_ENV_GPT_MODEL_CAPACITY || '100' }} outputs: CONTAINER_WEB_APPURL: ${{ steps.get_output_linux.outputs.CONTAINER_WEB_APPURL }} steps: @@ -252,8 +252,8 @@ jobs: azd env set AZURE_LOCATION="$AZURE_LOCATION" azd env set AZURE_RESOURCE_GROUP="$RESOURCE_GROUP_NAME" azd env set AZURE_ENV_IMAGETAG="$IMAGE_TAG" - # Use available quota capacity discovered by checkquota.sh; fall back to 300 if not set - azd env set AZURE_ENV_GPT_MODEL_CAPACITY="${AZURE_ENV_GPT_MODEL_CAPACITY:-300}" + # Use available quota capacity discovered by checkquota.sh; fall back to 100 if not set + azd env set AZURE_ENV_GPT_MODEL_CAPACITY="${AZURE_ENV_GPT_MODEL_CAPACITY:-100}" if [[ "$BUILD_DOCKER_IMAGE" == "true" ]]; then ACR_NAME=$(echo "${{ secrets.ACR_TEST_LOGIN_SERVER }}") diff --git a/.github/workflows/job-deploy-windows.yml b/.github/workflows/job-deploy-windows.yml index 33630cca..c200e536 100644 --- a/.github/workflows/job-deploy-windows.yml +++ b/.github/workflows/job-deploy-windows.yml @@ -37,7 +37,7 @@ on: AZURE_ENV_GPT_MODEL_CAPACITY: required: false type: string - default: "300" + default: "100" outputs: CONTAINER_WEB_APPURL: description: "Container Web App URL" @@ -49,7 +49,7 @@ jobs: environment: production env: AZURE_DEV_COLLECT_TELEMETRY: ${{ vars.AZURE_DEV_COLLECT_TELEMETRY }} - AZURE_ENV_GPT_MODEL_CAPACITY: ${{ inputs.AZURE_ENV_GPT_MODEL_CAPACITY || '300' }} + AZURE_ENV_GPT_MODEL_CAPACITY: ${{ inputs.AZURE_ENV_GPT_MODEL_CAPACITY || '100' }} outputs: CONTAINER_WEB_APPURL: ${{ steps.get_output_windows.outputs.CONTAINER_WEB_APPURL }} steps: @@ -249,8 +249,8 @@ jobs: azd env set AZURE_LOCATION="$env:AZURE_LOCATION" azd env set AZURE_RESOURCE_GROUP="$env:RESOURCE_GROUP_NAME" azd env set AZURE_ENV_IMAGETAG="$env:IMAGE_TAG" - # Use available quota capacity discovered by checkquota.sh; fall back to 300 if not set - $gptCapacity = if ($env:AZURE_ENV_GPT_MODEL_CAPACITY) { $env:AZURE_ENV_GPT_MODEL_CAPACITY } else { "300" } + # Use available quota capacity discovered by checkquota.sh; fall back to 100 if not set + $gptCapacity = if ($env:AZURE_ENV_GPT_MODEL_CAPACITY) { $env:AZURE_ENV_GPT_MODEL_CAPACITY } else { "100" } azd env set AZURE_ENV_GPT_MODEL_CAPACITY="$gptCapacity" # Set ACR name only when building Docker image diff --git a/.github/workflows/job-deploy.yml b/.github/workflows/job-deploy.yml index 2eb08b44..f58be0d8 100644 --- a/.github/workflows/job-deploy.yml +++ b/.github/workflows/job-deploy.yml @@ -90,7 +90,7 @@ on: value: ${{ jobs.azure-setup.outputs.QUOTA_FAILED }} env: - GPT_MIN_CAPACITY: 300 + 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 }} From 4098eb7a421441ac48be0b8dfffb77530ba3ad70 Mon Sep 17 00:00:00 2001 From: "Ashwal Vishwanath (Persistent Systems Inc)" Date: Mon, 15 Jun 2026 23:54:34 +0530 Subject: [PATCH 08/14] fix: reduce Bicep GPT default capacity from 300 to 100 for consistency --- infra/main.bicep | 2 +- infra/main.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/infra/main.bicep b/infra/main.bicep index 2c9292fd..e5280bda 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -76,7 +76,7 @@ param gptModelVersion string = '2025-11-13' @minValue(1) @description('Optional. Capacity of the GPT deployment: (minimum 10).') -param gptDeploymentCapacity int = 300 +param gptDeploymentCapacity int = 100 @description('Optional. The container registry login server/endpoint for the container images (for example, an Azure Container Registry endpoint).') param containerRegistryEndpoint string = 'cpscontainerreg.azurecr.io' diff --git a/infra/main.json b/infra/main.json index 2cf7180c..2fbbcf77 100644 --- a/infra/main.json +++ b/infra/main.json @@ -6,7 +6,7 @@ "_generator": { "name": "bicep", "version": "0.41.2.15936", - "templateHash": "2597731928932727268" + "templateHash": "2087291374146743089" }, "name": "Content Processing Solution Accelerator", "description": "Bicep template to deploy the Content Processing Solution Accelerator with AVM compliance." @@ -111,7 +111,7 @@ }, "gptDeploymentCapacity": { "type": "int", - "defaultValue": 300, + "defaultValue": 100, "minValue": 1, "metadata": { "description": "Optional. Capacity of the GPT deployment: (minimum 10)." From 1442ce83c092fd78e5040bda237f90c7243d7868 Mon Sep 17 00:00:00 2001 From: "Ashwal Vishwanath (Persistent Systems Inc)" Date: Mon, 15 Jun 2026 23:58:35 +0530 Subject: [PATCH 09/14] fix: checkquota.sh should exit with 1 when no region with sufficient quota is found --- infra/scripts/checkquota.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/infra/scripts/checkquota.sh b/infra/scripts/checkquota.sh index 194f2282..09dfaf08 100644 --- a/infra/scripts/checkquota.sh +++ b/infra/scripts/checkquota.sh @@ -5,12 +5,14 @@ ALLOWED_REGIONS=("australiaeast" "centralus" "eastasia" "eastus2" "japaneast" "northeurope" "southeastasia" "swedencentral" "uksouth") # Get requested regions from environment variable, default to all allowed regions +# Supports comma-separated or space-separated (or mixed) AZURE_REGIONS values. if [[ -n "$AZURE_REGIONS" ]]; then - IFS=',' read -ra REQUESTED_REGIONS <<< "$AZURE_REGIONS" + IFS=', ' read -ra REQUESTED_REGIONS <<< "$AZURE_REGIONS" # Filter requested regions to only include those in ALLOWED_REGIONS REGIONS=() for req_region in "${REQUESTED_REGIONS[@]}"; do req_region=$(echo "$req_region" | xargs) # trim whitespace + [[ -z "$req_region" ]] && continue # skip empty tokens from double-delimiters for allowed in "${ALLOWED_REGIONS[@]}"; do if [[ "$req_region" == "$allowed" ]]; then REGIONS+=("$req_region") @@ -109,7 +111,7 @@ done if [ -z "$VALID_REGION" ]; then echo "❌ No region with sufficient quota found. Blocking deployment." echo "QUOTA_FAILED=true" >> "$GITHUB_ENV" - exit 0 + exit 1 else echo "✅ Suggested Region: $VALID_REGION" echo "✅ Available Capacity: $VALID_REGION_AVAILABLE_CAPACITY" From c37460a8ddd266c5c7517fb1105cda822f5b3f94 Mon Sep 17 00:00:00 2001 From: "Ashwal Vishwanath (Persistent Systems Inc)" Date: Tue, 16 Jun 2026 00:00:47 +0530 Subject: [PATCH 10/14] revert: restore GPT deployment capacity from 100 back to 300 across all configuration files --- .github/workflows/deploy.yml | 4 ++-- .github/workflows/job-deploy-linux.yml | 8 ++++---- .github/workflows/job-deploy-windows.yml | 8 ++++---- infra/main.bicep | 2 +- infra/main.json | 4 ++-- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 80fa701d..5f1feb28 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -46,7 +46,7 @@ jobs: id: quota-check env: AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - GPT_MIN_CAPACITY: "100" + GPT_MIN_CAPACITY: "300" AZURE_REGIONS: ${{ vars.AZURE_REGIONS }} run: | chmod +x infra/scripts/checkquota.sh @@ -150,7 +150,7 @@ jobs: deploymentType="GlobalStandard" \ gptModelName="gpt-5.1" \ gptModelVersion="2025-11-13" \ - gptDeploymentCapacity="100" \ + gptDeploymentCapacity="300" \ azureAiServiceLocation="${{ env.AZURE_LOCATION }}" \ imageTag="latest_v2" \ tags="{'CreatedBy':'Pipeline', 'Purpose':'Deploying and Cleaning Up Resources for Validation','CreatedDate':'$current_date'}" \ diff --git a/.github/workflows/job-deploy-linux.yml b/.github/workflows/job-deploy-linux.yml index 559a9bc9..7b58ab1e 100644 --- a/.github/workflows/job-deploy-linux.yml +++ b/.github/workflows/job-deploy-linux.yml @@ -37,7 +37,7 @@ on: AZURE_ENV_GPT_MODEL_CAPACITY: required: false type: string - default: "100" + default: "300" outputs: CONTAINER_WEB_APPURL: description: "Container Web App URL" @@ -49,7 +49,7 @@ jobs: environment: production env: AZURE_DEV_COLLECT_TELEMETRY: ${{ vars.AZURE_DEV_COLLECT_TELEMETRY }} - AZURE_ENV_GPT_MODEL_CAPACITY: ${{ inputs.AZURE_ENV_GPT_MODEL_CAPACITY || '100' }} + AZURE_ENV_GPT_MODEL_CAPACITY: ${{ inputs.AZURE_ENV_GPT_MODEL_CAPACITY || '300' }} outputs: CONTAINER_WEB_APPURL: ${{ steps.get_output_linux.outputs.CONTAINER_WEB_APPURL }} steps: @@ -252,8 +252,8 @@ jobs: azd env set AZURE_LOCATION="$AZURE_LOCATION" azd env set AZURE_RESOURCE_GROUP="$RESOURCE_GROUP_NAME" azd env set AZURE_ENV_IMAGETAG="$IMAGE_TAG" - # Use available quota capacity discovered by checkquota.sh; fall back to 100 if not set - azd env set AZURE_ENV_GPT_MODEL_CAPACITY="${AZURE_ENV_GPT_MODEL_CAPACITY:-100}" + # Use available quota capacity discovered by checkquota.sh; fall back to 300 if not set + azd env set AZURE_ENV_GPT_MODEL_CAPACITY="${AZURE_ENV_GPT_MODEL_CAPACITY:-300}" if [[ "$BUILD_DOCKER_IMAGE" == "true" ]]; then ACR_NAME=$(echo "${{ secrets.ACR_TEST_LOGIN_SERVER }}") diff --git a/.github/workflows/job-deploy-windows.yml b/.github/workflows/job-deploy-windows.yml index c200e536..33630cca 100644 --- a/.github/workflows/job-deploy-windows.yml +++ b/.github/workflows/job-deploy-windows.yml @@ -37,7 +37,7 @@ on: AZURE_ENV_GPT_MODEL_CAPACITY: required: false type: string - default: "100" + default: "300" outputs: CONTAINER_WEB_APPURL: description: "Container Web App URL" @@ -49,7 +49,7 @@ jobs: environment: production env: AZURE_DEV_COLLECT_TELEMETRY: ${{ vars.AZURE_DEV_COLLECT_TELEMETRY }} - AZURE_ENV_GPT_MODEL_CAPACITY: ${{ inputs.AZURE_ENV_GPT_MODEL_CAPACITY || '100' }} + AZURE_ENV_GPT_MODEL_CAPACITY: ${{ inputs.AZURE_ENV_GPT_MODEL_CAPACITY || '300' }} outputs: CONTAINER_WEB_APPURL: ${{ steps.get_output_windows.outputs.CONTAINER_WEB_APPURL }} steps: @@ -249,8 +249,8 @@ jobs: azd env set AZURE_LOCATION="$env:AZURE_LOCATION" azd env set AZURE_RESOURCE_GROUP="$env:RESOURCE_GROUP_NAME" azd env set AZURE_ENV_IMAGETAG="$env:IMAGE_TAG" - # Use available quota capacity discovered by checkquota.sh; fall back to 100 if not set - $gptCapacity = if ($env:AZURE_ENV_GPT_MODEL_CAPACITY) { $env:AZURE_ENV_GPT_MODEL_CAPACITY } else { "100" } + # Use available quota capacity discovered by checkquota.sh; fall back to 300 if not set + $gptCapacity = if ($env:AZURE_ENV_GPT_MODEL_CAPACITY) { $env:AZURE_ENV_GPT_MODEL_CAPACITY } else { "300" } azd env set AZURE_ENV_GPT_MODEL_CAPACITY="$gptCapacity" # Set ACR name only when building Docker image diff --git a/infra/main.bicep b/infra/main.bicep index e5280bda..2c9292fd 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -76,7 +76,7 @@ param gptModelVersion string = '2025-11-13' @minValue(1) @description('Optional. Capacity of the GPT deployment: (minimum 10).') -param gptDeploymentCapacity int = 100 +param gptDeploymentCapacity int = 300 @description('Optional. The container registry login server/endpoint for the container images (for example, an Azure Container Registry endpoint).') param containerRegistryEndpoint string = 'cpscontainerreg.azurecr.io' diff --git a/infra/main.json b/infra/main.json index 2fbbcf77..2cf7180c 100644 --- a/infra/main.json +++ b/infra/main.json @@ -6,7 +6,7 @@ "_generator": { "name": "bicep", "version": "0.41.2.15936", - "templateHash": "2087291374146743089" + "templateHash": "2597731928932727268" }, "name": "Content Processing Solution Accelerator", "description": "Bicep template to deploy the Content Processing Solution Accelerator with AVM compliance." @@ -111,7 +111,7 @@ }, "gptDeploymentCapacity": { "type": "int", - "defaultValue": 100, + "defaultValue": 300, "minValue": 1, "metadata": { "description": "Optional. Capacity of the GPT deployment: (minimum 10)." From bfdaa8bd468096a563f84f8fefe33cbfb65f7ab6 Mon Sep 17 00:00:00 2001 From: "Ashwal Vishwanath (Persistent Systems Inc)" Date: Tue, 16 Jun 2026 11:44:48 +0530 Subject: [PATCH 11/14] fix: support json schema uploads in post-deployment registration --- infra/scripts/post_deployment.ps1 | 118 ++++++++++++++++++++++++++++-- infra/scripts/post_deployment.sh | 72 +++++++++++++++++- 2 files changed, 182 insertions(+), 8 deletions(-) diff --git a/infra/scripts/post_deployment.ps1 b/infra/scripts/post_deployment.ps1 index 04104a50..87923450 100644 --- a/infra/scripts/post_deployment.ps1 +++ b/infra/scripts/post_deployment.ps1 @@ -76,13 +76,55 @@ if (-not $ApiReady) { Write-Host " API did not become ready after $MaxRetries attempts. Skipping schema registration." Write-Host " Run manually after the API is ready." } else { - # ---------- Schema registration (no Python dependency) ---------- + # ---------- Schema registration ---------- $SchemaInfoFile = Join-Path $FullPath "schema_info.json" $Manifest = Get-Content $SchemaInfoFile -Raw | ConvertFrom-Json $SchemaVaultUrl = "$ApiBaseUrl/schemavault/" $SchemaSetVaultUrl = "$ApiBaseUrl/schemasetvault/" + $PythonBin = $null + if (Get-Command python3 -ErrorAction SilentlyContinue) { + $PythonBin = "python3" + } elseif (Get-Command python -ErrorAction SilentlyContinue) { + $PythonBin = "python" + } + + function Convert-PythonSchemaToJson { + param( + [Parameter(Mandatory = $true)] [string]$PythonFile, + [Parameter(Mandatory = $true)] [string]$ClassName, + [Parameter(Mandatory = $true)] [string]$OutputFile, + [Parameter(Mandatory = $true)] [string]$PythonCmd + ) + + $script = @' +import importlib.util +import json +import sys + +py_path, class_name, out_path = sys.argv[1], sys.argv[2], sys.argv[3] +spec = importlib.util.spec_from_file_location("schema_module", py_path) +if spec is None or spec.loader is None: + raise RuntimeError(f"Unable to load schema module from {py_path}") +module = importlib.util.module_from_spec(spec) +spec.loader.exec_module(module) +cls = getattr(module, class_name, None) +if cls is None: + raise RuntimeError(f"Class '{class_name}' not found in {py_path}") +if not hasattr(cls, "model_json_schema"): + raise RuntimeError(f"Class '{class_name}' does not expose model_json_schema()") +schema = cls.model_json_schema() +with open(out_path, "w", encoding="utf-8") as f: + json.dump(schema, f, indent=2) +'@ + + $script | & $PythonCmd - $PythonFile $ClassName $OutputFile + if ($LASTEXITCODE -ne 0) { + throw "Failed generating JSON schema from '$PythonFile'." + } + } + # --- Step 1: Register schemas --- Write-Host "" Write-Host ("=" * 60) @@ -104,6 +146,7 @@ if (-not $ApiReady) { $ClassName = $entry.ClassName $Description = $entry.Description $SchemaFile = Join-Path $FullPath $entry.File + $SchemaFileOriginal = $SchemaFile Write-Host "" Write-Host "Processing schema: $ClassName" @@ -122,12 +165,34 @@ if (-not $ApiReady) { continue } + $UploadFile = $SchemaFile + $UploadFileName = [System.IO.Path]::GetFileName($SchemaFile) + $UploadContentType = "application/json" + $IsGeneratedJson = $false + + if ([System.IO.Path]::GetExtension($SchemaFile).ToLowerInvariant() -eq ".py") { + if (-not $PythonBin) { + Write-Host " Error: Python is required to convert '$UploadFileName' to JSON schema. Skipping..." + continue + } + + $GeneratedJsonPath = Join-Path $FullPath ("{0}.json" -f $ClassName) + try { + Convert-PythonSchemaToJson -PythonFile $SchemaFile -ClassName $ClassName -OutputFile $GeneratedJsonPath -PythonCmd $PythonBin + $UploadFile = $GeneratedJsonPath + $UploadFileName = [System.IO.Path]::GetFileNameWithoutExtension($SchemaFile) + ".json" + $IsGeneratedJson = $true + } catch { + Write-Host " Error: $_" + continue + } + } + Write-Host " Registering new schema '$ClassName'..." # Build multipart form data $dataPayload = @{ ClassName = $ClassName; Description = $Description } | ConvertTo-Json -Compress - $fileBytes = [System.IO.File]::ReadAllBytes($SchemaFile) - $fileName = [System.IO.Path]::GetFileName($SchemaFile) + $fileBytes = [System.IO.File]::ReadAllBytes($UploadFile) $boundary = [System.Guid]::NewGuid().ToString() $LF = "`r`n" @@ -136,8 +201,8 @@ if (-not $ApiReady) { "Content-Disposition: form-data; name=`"data`"$LF", $dataPayload, "--$boundary", - "Content-Disposition: form-data; name=`"file`"; filename=`"$fileName`"", - "Content-Type: text/x-python$LF", + "Content-Disposition: form-data; name=`"file`"; filename=`"$UploadFileName`"", + "Content-Type: $UploadContentType$LF", [System.Text.Encoding]::UTF8.GetString($fileBytes), "--$boundary--$LF" ) -join $LF @@ -150,7 +215,48 @@ if (-not $ApiReady) { Write-Host " Successfully registered: $Description's Schema Id - $schemaId" $Registered[$ClassName] = $schemaId } catch { - Write-Host " Failed to upload '$fileName'. Error: $_" + $statusCode = $null + $responseBody = "" + if ($_.Exception.Response) { + $statusCode = [int]$_.Exception.Response.StatusCode + try { + $reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream()) + $responseBody = $reader.ReadToEnd() + $reader.Close() + } catch { + $responseBody = "" + } + } + + if ($IsGeneratedJson -and $statusCode -eq 415 -and $responseBody -match "Only \.py schema files are supported") { + Write-Host " API expects legacy .py schemas. Retrying with '$([System.IO.Path]::GetFileName($SchemaFileOriginal))'..." + try { + $legacyBytes = [System.IO.File]::ReadAllBytes($SchemaFileOriginal) + $legacyName = [System.IO.Path]::GetFileName($SchemaFileOriginal) + $legacyBoundary = [System.Guid]::NewGuid().ToString() + $legacyBody = ( + "--$legacyBoundary", + "Content-Disposition: form-data; name=`"data`"$LF", + $dataPayload, + "--$legacyBoundary", + "Content-Disposition: form-data; name=`"file`"; filename=`"$legacyName`"", + "Content-Type: text/x-python$LF", + [System.Text.Encoding]::UTF8.GetString($legacyBytes), + "--$legacyBoundary--$LF" + ) -join $LF + + $legacyResp = Invoke-RestMethod -Uri $SchemaVaultUrl -Method POST ` + -ContentType "multipart/form-data; boundary=$legacyBoundary" ` + -Body $legacyBody -TimeoutSec 60 -ErrorAction Stop + $schemaId = $legacyResp.Id + Write-Host " Successfully registered (legacy): $Description's Schema Id - $schemaId" + $Registered[$ClassName] = $schemaId + } catch { + Write-Host " Failed to upload '$legacyName'. Error: $_" + } + } else { + Write-Host " Failed to upload '$UploadFileName'. Error: $_" + } } } diff --git a/infra/scripts/post_deployment.sh b/infra/scripts/post_deployment.sh index 82bad847..b873b602 100644 --- a/infra/scripts/post_deployment.sh +++ b/infra/scripts/post_deployment.sh @@ -80,11 +80,45 @@ if [ "$STATUS" != "200" ]; then echo " API did not become ready after $MAX_RETRIES attempts. Skipping schema registration." echo " Run manually after the API is ready." else - # ---------- Schema registration (no Python dependency) ---------- + # ---------- Schema registration ---------- SCHEMA_INFO_FILE="$DATA_SCRIPT_PATH/schema_info.json" SCHEMAVAULT_URL="$API_BASE_URL/schemavault/" SCHEMASETVAULT_URL="$API_BASE_URL/schemasetvault/" + PYTHON_BIN="" + if command -v python3 >/dev/null 2>&1; then + PYTHON_BIN="python3" + elif command -v python >/dev/null 2>&1; then + PYTHON_BIN="python" + fi + + generate_json_schema_from_python() { + local py_file="$1" + local class_name="$2" + local output_file="$3" + + "$PYTHON_BIN" - "$py_file" "$class_name" "$output_file" <<'PY' +import importlib.util +import json +import sys + +py_path, class_name, out_path = sys.argv[1], sys.argv[2], sys.argv[3] +spec = importlib.util.spec_from_file_location("schema_module", py_path) +if spec is None or spec.loader is None: + raise RuntimeError(f"Unable to load schema module from {py_path}") +module = importlib.util.module_from_spec(spec) +spec.loader.exec_module(module) +cls = getattr(module, class_name, None) +if cls is None: + raise RuntimeError(f"Class '{class_name}' not found in {py_path}") +if not hasattr(cls, "model_json_schema"): + raise RuntimeError(f"Class '{class_name}' does not expose model_json_schema()") +schema = cls.model_json_schema() +with open(out_path, "w", encoding="utf-8") as f: + json.dump(schema, f, indent=2) +PY + } + # --- Step 1: Register schemas --- echo "" echo "============================================================" @@ -109,6 +143,7 @@ else DESCRIPTION=$(echo "$ENTRY" | grep -o '"Description"[[:space:]]*:[[:space:]]*"[^"]*"' | sed -n "$((idx + 1))p" | sed 's/.*"Description"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/') SCHEMA_FILE="$DATA_SCRIPT_PATH/$FILE_NAME" + SCHEMA_FILE_ORIGINAL="$SCHEMA_FILE" echo "" echo "Processing schema: $CLASS_NAME" @@ -133,18 +168,51 @@ else continue fi + UPLOAD_FILE="$SCHEMA_FILE" + UPLOAD_FILENAME="$FILE_NAME" + UPLOAD_CONTENT_TYPE="application/json" + IS_GENERATED_JSON=false + + if [[ "${SCHEMA_FILE,,}" == *.py ]]; then + if [ -z "$PYTHON_BIN" ]; then + echo " Error: Python is required to convert '$FILE_NAME' to JSON schema. Skipping..." + continue + fi + + GENERATED_JSON_FILE="$DATA_SCRIPT_PATH/${CLASS_NAME}.json" + if generate_json_schema_from_python "$SCHEMA_FILE" "$CLASS_NAME" "$GENERATED_JSON_FILE"; then + UPLOAD_FILE="$GENERATED_JSON_FILE" + UPLOAD_FILENAME="${FILE_NAME%.py}.json" + IS_GENERATED_JSON=true + else + echo " Error: Failed to generate JSON schema from '$FILE_NAME'. Skipping..." + continue + fi + fi + echo " Registering new schema '$CLASS_NAME'..." DATA_PAYLOAD="{\"ClassName\": \"$CLASS_NAME\", \"Description\": \"$DESCRIPTION\"}" RESPONSE=$(curl -s -w "\n%{http_code}" \ -X POST "$SCHEMAVAULT_URL" \ -F "data=$DATA_PAYLOAD" \ - -F "file=@$SCHEMA_FILE;type=text/x-python" \ + -F "file=@$UPLOAD_FILE;filename=$UPLOAD_FILENAME;type=$UPLOAD_CONTENT_TYPE" \ --connect-timeout 60) HTTP_CODE=$(echo "$RESPONSE" | tail -1) BODY=$(echo "$RESPONSE" | sed '$d') + if [ "$HTTP_CODE" = "415" ] && [ "$IS_GENERATED_JSON" = true ] && echo "$BODY" | grep -q "Only \.py schema files are supported"; then + echo " API expects legacy .py schemas. Retrying with '$FILE_NAME'..." + RESPONSE=$(curl -s -w "\n%{http_code}" \ + -X POST "$SCHEMAVAULT_URL" \ + -F "data=$DATA_PAYLOAD" \ + -F "file=@$SCHEMA_FILE_ORIGINAL;filename=$FILE_NAME;type=text/x-python" \ + --connect-timeout 60) + HTTP_CODE=$(echo "$RESPONSE" | tail -1) + BODY=$(echo "$RESPONSE" | sed '$d') + fi + if [ "$HTTP_CODE" = "200" ]; then SCHEMA_ID=$(echo "$BODY" | sed 's/.*"Id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/') echo " Successfully registered: $DESCRIPTION's Schema Id - $SCHEMA_ID" From d51d7c487bdb8a6404efb35dfdf6b26520643ad0 Mon Sep 17 00:00:00 2001 From: "Ashwal Vishwanath (Persistent Systems Inc)" Date: Tue, 16 Jun 2026 11:50:14 +0530 Subject: [PATCH 12/14] fix: make post-deployment hooks best-effort across bash and powershell --- infra/scripts/post_deployment.ps1 | 7 +++++-- infra/scripts/post_deployment.sh | 12 +++++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/infra/scripts/post_deployment.ps1 b/infra/scripts/post_deployment.ps1 index 87923450..a8e8f18d 100644 --- a/infra/scripts/post_deployment.ps1 +++ b/infra/scripts/post_deployment.ps1 @@ -1,5 +1,6 @@ -# Stop script on any error -$ErrorActionPreference = "Stop" +# Keep post-deployment best-effort so provisioning does not fail. +$ErrorActionPreference = "Continue" +$PSNativeCommandUseErrorActionPreference = $false Write-Host "[Search] Fetching container app info from azd environment..." @@ -336,3 +337,5 @@ with open(out_path, "w", encoding="utf-8") as f: Write-Host " Schemas registered: $($Registered.Count)" Write-Host ("=" * 60) } + +exit 0 diff --git a/infra/scripts/post_deployment.sh b/infra/scripts/post_deployment.sh index b873b602..56ba6e1e 100644 --- a/infra/scripts/post_deployment.sh +++ b/infra/scripts/post_deployment.sh @@ -1,7 +1,7 @@ #!/bin/bash -# Stop script on any error -set -e +# Keep post-deployment best-effort so provisioning does not fail. +set +e echo "🔍 Fetching container app info from azd environment..." @@ -135,6 +135,10 @@ PY REGISTERED_IDS=() REGISTERED_NAMES=() + if [ "$SCHEMA_COUNT" -eq 0 ]; then + echo "No schemas found in manifest. Skipping schema registration." + fi + for idx in $(seq 0 $((SCHEMA_COUNT - 1))); do # Parse entry fields using grep/sed (no python needed) ENTRY=$(cat "$SCHEMA_INFO_FILE") @@ -321,7 +325,7 @@ else echo " Checking account: $CU_ACCOUNT_NAME in resource group: $RESOURCE_GROUP" # Check if the resource group exists first - if ! az group exists -n "$RESOURCE_GROUP" --output none 2>/dev/null; then + if ! az group show -n "$RESOURCE_GROUP" --output none 2>/dev/null; then echo " ⚠️ Resource group '$RESOURCE_GROUP' does not exist yet. Skipping refresh (expected on fresh deployment)." elif ! az cognitiveservices account show -g "$RESOURCE_GROUP" -n "$CU_ACCOUNT_NAME" --output none 2>/dev/null; then echo " ⚠️ Cognitive Services account '$CU_ACCOUNT_NAME' not found. Skipping refresh (expected on fresh deployment)." @@ -335,3 +339,5 @@ else echo " ⚠️ Failed to refresh Cognitive Services account '$CU_ACCOUNT_NAME' (non-critical)." fi fi + +exit 0 From 834e1658a3fbb7ab21e9fcf9d3df5449f753732a Mon Sep 17 00:00:00 2001 From: "Ashwal Vishwanath (Persistent Systems Inc)" Date: Tue, 16 Jun 2026 12:35:15 +0530 Subject: [PATCH 13/14] fix: make post-deployment schema generation resilient --- infra/scripts/post_deployment.ps1 | 11 +++++++++++ infra/scripts/post_deployment.sh | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/infra/scripts/post_deployment.ps1 b/infra/scripts/post_deployment.ps1 index a8e8f18d..c91b892e 100644 --- a/infra/scripts/post_deployment.ps1 +++ b/infra/scripts/post_deployment.ps1 @@ -102,14 +102,25 @@ if (-not $ApiReady) { $script = @' import importlib.util import json +import inspect import sys +from pydantic import BaseModel + py_path, class_name, out_path = sys.argv[1], sys.argv[2], sys.argv[3] spec = importlib.util.spec_from_file_location("schema_module", py_path) if spec is None or spec.loader is None: raise RuntimeError(f"Unable to load schema module from {py_path}") module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) + +for obj in module.__dict__.values(): + if inspect.isclass(obj) and issubclass(obj, BaseModel): + try: + obj.model_rebuild(_types_namespace=module.__dict__) + except Exception: + pass + cls = getattr(module, class_name, None) if cls is None: raise RuntimeError(f"Class '{class_name}' not found in {py_path}") diff --git a/infra/scripts/post_deployment.sh b/infra/scripts/post_deployment.sh index 56ba6e1e..6621f58b 100644 --- a/infra/scripts/post_deployment.sh +++ b/infra/scripts/post_deployment.sh @@ -100,14 +100,25 @@ else "$PYTHON_BIN" - "$py_file" "$class_name" "$output_file" <<'PY' import importlib.util import json +import inspect import sys +from pydantic import BaseModel + py_path, class_name, out_path = sys.argv[1], sys.argv[2], sys.argv[3] spec = importlib.util.spec_from_file_location("schema_module", py_path) if spec is None or spec.loader is None: raise RuntimeError(f"Unable to load schema module from {py_path}") module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) + +for obj in module.__dict__.values(): + if inspect.isclass(obj) and issubclass(obj, BaseModel): + try: + obj.model_rebuild(_types_namespace=module.__dict__) + except Exception: + pass + cls = getattr(module, class_name, None) if cls is None: raise RuntimeError(f"Class '{class_name}' not found in {py_path}") From be96822ddc414b1980f4336f1fa7d52fe58df445 Mon Sep 17 00:00:00 2001 From: "Ashwal Vishwanath (Persistent Systems Inc)" Date: Tue, 16 Jun 2026 13:10:05 +0530 Subject: [PATCH 14/14] fix: add private endpoint dependency guard to main_custom template --- infra/main_custom.bicep | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/infra/main_custom.bicep b/infra/main_custom.bicep index 3294106b..d491e348 100644 --- a/infra/main_custom.bicep +++ b/infra/main_custom.bicep @@ -830,6 +830,11 @@ module cognitiveServicePrivateEndpoint 'br/public:avm/res/network/private-endpoi } subnetResourceId: virtualNetwork!.outputs.backendSubnetResourceId } + dependsOn: [ + avmAiServices + virtualNetwork + avmPrivateDnsZones + ] } module avmAiServices_cu 'br/public:avm/res/cognitive-services/account:0.14.1' = {