diff --git a/.github/workflows/deploy-linux.yml b/.github/workflows/deploy-linux.yml
new file mode 100644
index 00000000..3674f1c7
--- /dev/null
+++ b/.github/workflows/deploy-linux.yml
@@ -0,0 +1,267 @@
+name: Deploy-Test-Cleanup (v2) Linux
+permissions:
+ contents: read
+ actions: read
+on:
+ workflow_run:
+ workflows: ["Build Docker and Optional Push"]
+ types:
+ - completed
+ branches:
+ - main
+ - dev
+ - demo
+ 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 & 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 Container WebApp URL (Skips Deployment)'
+ required: false
+ default: ''
+ type: string
+
+ schedule:
+ - cron: '0 5,17 * * *' # Runs at 5:00 AM and 5:00 PM GMT
+
+jobs:
+ validate-inputs:
+ 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 }}
+ build_docker_image: ${{ steps.validate.outputs.build_docker_image }}
+ 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 }}
+ azure_existing_ai_project_resource_id: ${{ steps.validate.outputs.azure_existing_ai_project_resource_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_BUILD_DOCKER_IMAGE: ${{ github.event.inputs.build_docker_image }}
+ 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_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ github.event.inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_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 build_docker_image (boolean)
+ BUILD_DOCKER="${INPUT_BUILD_DOCKER_IMAGE:-false}"
+ if [[ "$BUILD_DOCKER" != "true" && "$BUILD_DOCKER" != "false" ]]; then
+ echo "β ERROR: build_docker_image must be 'true' or 'false', got: '$BUILD_DOCKER'"
+ VALIDATION_FAILED=true
+ else
+ echo "β
build_docker_image: '$BUILD_DOCKER' 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/workspaces/[^/]+$ ]]; 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 AZURE_EXISTING_AI_PROJECT_RESOURCE_ID (optional, Azure Resource ID format)
+ if [[ -n "$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" ]]; then
+ if [[ ! "$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" =~ ^/subscriptions/[a-fA-F0-9-]+/resourceGroups/[^/]+/providers/(Microsoft\.MachineLearningServices/(workspaces|projects)/[^/]+|Microsoft\.CognitiveServices/accounts/[^/]+/projects/[^/]+)$ ]]; then
+ echo "β ERROR: AZURE_EXISTING_AI_PROJECT_RESOURCE_ID is invalid. Must be a valid Azure Resource ID format:"
+ echo " /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.CognitiveServices/accounts/{accountName}/projects/{projectName}"
+ echo " Got: '$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID'"
+ VALIDATION_FAILED=true
+ else
+ echo "β
AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: Valid Resource ID format"
+ fi
+ else
+ echo "β
AZURE_EXISTING_AI_PROJECT_RESOURCE_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 "build_docker_image=$BUILD_DOCKER" >> $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 "azure_existing_ai_project_resource_id=$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_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:
+ runner_os: ubuntu-latest
+ azure_location: ${{ needs.validate-inputs.outputs.azure_location }}
+ 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' }}
+ build_docker_image: ${{ needs.validate-inputs.outputs.build_docker_image == 'true' }}
+ cleanup_resources: ${{ needs.validate-inputs.outputs.cleanup_resources == 'true' }}
+ run_e2e_tests: ${{ needs.validate-inputs.outputs.run_e2e_tests }}
+ AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ needs.validate-inputs.outputs.azure_env_log_analytics_workspace_id }}
+ AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ needs.validate-inputs.outputs.azure_existing_ai_project_resource_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
new file mode 100644
index 00000000..fea49ef8
--- /dev/null
+++ b/.github/workflows/deploy-orchestrator.yml
@@ -0,0 +1,144 @@
+name: Deployment orchestrator
+
+permissions:
+ contents: read
+ actions: read
+
+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: "!cancelled() && (needs.docker-build.result == 'success' || needs.docker-build.result == 'skipped') && (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: "!cancelled() && ((needs.deploy.result == 'success' && needs.deploy.outputs.CONTAINER_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:
+ TEST_URL: ${{ needs.deploy.outputs.CONTAINER_WEB_APPURL || inputs.existing_webapp_url }}
+ TEST_SUITE: ${{ inputs.trigger_type == 'workflow_dispatch' && inputs.run_e2e_tests || 'GoldenPath-Testing' }}
+ secrets: inherit
+
+ send-notification:
+ if: "!cancelled()"
+ 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 }}
+ CONTAINER_WEB_APPURL: ${{ needs.deploy.outputs.CONTAINER_WEB_APPURL }}
+ 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.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..2987af50
--- /dev/null
+++ b/.github/workflows/deploy-windows.yml
@@ -0,0 +1,263 @@
+name: Deploy-Test-Cleanup (v2) Windows
+permissions:
+ contents: read
+ actions: read
+on:
+ 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 & 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 Container WebApp URL (Skips Deployment)'
+ required: false
+ default: ''
+ type: string
+
+ # schedule:
+ # - cron: '0 5,17 * * *' # Runs at 5:00 AM and 5:00 PM GMT
+
+jobs:
+ validate-inputs:
+ 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 }}
+ build_docker_image: ${{ steps.validate.outputs.build_docker_image }}
+ 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 }}
+ azure_existing_ai_project_resource_id: ${{ steps.validate.outputs.azure_existing_ai_project_resource_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_BUILD_DOCKER_IMAGE: ${{ github.event.inputs.build_docker_image }}
+ 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_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ github.event.inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_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 build_docker_image (boolean)
+ BUILD_DOCKER="${INPUT_BUILD_DOCKER_IMAGE:-false}"
+ if [[ "$BUILD_DOCKER" != "true" && "$BUILD_DOCKER" != "false" ]]; then
+ echo "β ERROR: build_docker_image must be 'true' or 'false', got: '$BUILD_DOCKER'"
+ VALIDATION_FAILED=true
+ else
+ echo "β
build_docker_image: '$BUILD_DOCKER' 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/workspaces/[^/]+$ ]]; 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 AZURE_EXISTING_AI_PROJECT_RESOURCE_ID (optional, Azure Resource ID format)
+ if [[ -n "$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" ]]; then
+ if [[ ! "$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" =~ ^/subscriptions/[a-fA-F0-9-]+/resourceGroups/[^/]+/providers/(Microsoft\.MachineLearningServices/(workspaces|projects)/[^/]+|Microsoft\.CognitiveServices/accounts/[^/]+/projects/[^/]+)$ ]]; then
+ echo "β ERROR: AZURE_EXISTING_AI_PROJECT_RESOURCE_ID is invalid. Must be a valid Azure Resource ID format:"
+ echo " /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.CognitiveServices/accounts/{accountName}/projects/{projectName}"
+ echo " Got: '$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID'"
+ VALIDATION_FAILED=true
+ else
+ echo "β
AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: Valid Resource ID format"
+ fi
+ else
+ echo "β
AZURE_EXISTING_AI_PROJECT_RESOURCE_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 "build_docker_image=$BUILD_DOCKER" >> $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 "azure_existing_ai_project_resource_id=$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_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:
+ runner_os: windows-latest
+ azure_location: ${{ needs.validate-inputs.outputs.azure_location }}
+ 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' }}
+ build_docker_image: ${{ needs.validate-inputs.outputs.build_docker_image == 'true' }}
+ cleanup_resources: ${{ needs.validate-inputs.outputs.cleanup_resources == 'true' }}
+ run_e2e_tests: ${{ needs.validate-inputs.outputs.run_e2e_tests }}
+ AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ needs.validate-inputs.outputs.azure_env_log_analytics_workspace_id }}
+ AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ needs.validate-inputs.outputs.azure_existing_ai_project_resource_id }}
+ existing_webapp_url: ${{ needs.validate-inputs.outputs.existing_webapp_url }}
+ trigger_type: ${{ github.event_name }}
+ secrets: inherit
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 29facdff..b5291338 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -110,7 +110,7 @@ jobs:
id: generate_solution_prefix
run: |
set -e
- COMMON_PART="ccb"
+ COMMON_PART="byocc"
TIMESTAMP=$(date +%s)
UPDATED_TIMESTAMP=$(echo $TIMESTAMP | tail -c 4)
UNIQUE_SOLUTION_PREFIX="${COMMON_PART}${UPDATED_TIMESTAMP}"
diff --git a/.github/workflows/job-cleanup-deployment.yml b/.github/workflows/job-cleanup-deployment.yml
new file mode 100644
index 00000000..e1afa455
--- /dev/null
+++ b/.github/workflows/job-cleanup-deployment.yml
@@ -0,0 +1,111 @@
+name: Cleanup Deployment Job
+
+permissions:
+ contents: read
+ actions: read
+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: 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-linux.yml b/.github/workflows/job-deploy-linux.yml
new file mode 100644
index 00000000..66483f76
--- /dev/null
+++ b/.github/workflows/job-deploy-linux.yml
@@ -0,0 +1,546 @@
+name: Deploy Steps - Linux
+
+permissions:
+ contents: read
+ actions: read
+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
+ E2E_TEST:
+ required: false
+ type: string
+ default: 'false'
+ outputs:
+ CONTAINER_WEB_APPURL:
+ description: "Container Web App URL"
+ value: ${{ jobs.deploy-linux.outputs.WEBAPP_URL }}
+
+jobs:
+ deploy-linux:
+ runs-on: ubuntu-latest
+ env:
+ AZURE_DEV_COLLECT_TELEMETRY: ${{ vars.AZURE_DEV_COLLECT_TELEMETRY }}
+ outputs:
+ WEBAPP_URL: ${{ steps.get_output_linux.outputs.WEBAPP_URL }}
+ steps:
+ - name: Checkout Code
+ uses: actions/checkout@v4
+
+ - 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_BUILD_DOCKER_IMAGE: ${{ inputs.BUILD_DOCKER_IMAGE }}
+ INPUT_EXP: ${{ inputs.EXP }}
+ INPUT_WAF_ENABLED: ${{ inputs.WAF_ENABLED }}
+ INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}
+ INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }}
+ run: |
+ echo "π Validating workflow input parameters..."
+ VALIDATION_FAILED=false
+
+ # Validate ENV_NAME (required, alphanumeric and hyphens)
+ if [[ -z "$INPUT_ENV_NAME" ]]; then
+ echo "β ERROR: ENV_NAME is required but 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 alphanumerics 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 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
+ 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 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
+ else
+ echo "β
AZURE_LOCATION: '$INPUT_AZURE_LOCATION' is valid"
+ fi
+
+ # Validate RESOURCE_GROUP_NAME (required, Azure naming convention)
+ if [[ -z "$INPUT_RESOURCE_GROUP_NAME" ]]; then
+ echo "β ERROR: RESOURCE_GROUP_NAME is required but 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 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, max 128 characters"
+ VALIDATION_FAILED=true
+ else
+ echo "β
IMAGE_TAG: '$INPUT_IMAGE_TAG' is valid"
+ fi
+
+ # Validate BUILD_DOCKER_IMAGE (required, must be 'true' or 'false')
+ if [[ "$INPUT_BUILD_DOCKER_IMAGE" != "true" && "$INPUT_BUILD_DOCKER_IMAGE" != "false" ]]; then
+ echo "β ERROR: BUILD_DOCKER_IMAGE must be 'true' or 'false', got: '$INPUT_BUILD_DOCKER_IMAGE'"
+ VALIDATION_FAILED=true
+ else
+ echo "β
BUILD_DOCKER_IMAGE: '$INPUT_BUILD_DOCKER_IMAGE' is valid"
+ fi
+
+ # Validate EXP (required, must be 'true' or 'false')
+ 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 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, if provided must be valid Resource ID)
+ 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/workspaces/[^/]+$ ]]; 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 AZURE_EXISTING_AI_PROJECT_RESOURCE_ID (optional, if provided must be valid Resource ID)
+ if [[ -n "$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" ]]; then
+ if [[ ! "$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" =~ ^/subscriptions/[a-fA-F0-9-]+/resourceGroups/[^/]+/providers/(Microsoft\.MachineLearningServices/(workspaces|projects)/[^/]+|Microsoft\.CognitiveServices/accounts/[^/]+/projects/[^/]+)$ ]]; then
+ echo "β ERROR: AZURE_EXISTING_AI_PROJECT_RESOURCE_ID is invalid. Must be a valid Azure Resource ID format:"
+ echo " /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.CognitiveServices/accounts/{accountName}/projects/{projectName}"
+ echo " Got: '$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID'"
+ VALIDATION_FAILED=true
+ else
+ echo "β
AZURE_EXISTING_AI_PROJECT_RESOURCE_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: Configure Parameters Based on WAF Setting
+ shell: bash
+ env:
+ INPUT_WAF_ENABLED: ${{ inputs.WAF_ENABLED }}
+ run: |
+ 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
+ echo "π§ Configuring Non-WAF deployment - using default main.parameters.json..."
+ fi
+
+ - name: Install azd
+ 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 (Linux)
+ id: get_output_linux
+ 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_BUILD_DOCKER_IMAGE: ${{ inputs.BUILD_DOCKER_IMAGE }}
+ INPUT_EXP: ${{ inputs.EXP }}
+ INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}
+ INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }}
+ run: |
+ set -e
+
+ echo "Creating environment..."
+ azd env new "$INPUT_ENV_NAME" --no-prompt
+ echo "Environment created: $INPUT_ENV_NAME"
+
+ echo "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_AI_SERVICE_LOCATION="$INPUT_AZURE_ENV_OPENAI_LOCATION"
+ azd env set AZURE_LOCATION="$INPUT_AZURE_LOCATION"
+ azd env set AZURE_RESOURCE_GROUP="$INPUT_RESOURCE_GROUP_NAME"
+ azd env set AZURE_ENV_IMAGETAG="$INPUT_IMAGE_TAG"
+
+ if [[ "$INPUT_BUILD_DOCKER_IMAGE" == "true" ]]; then
+ ACR_NAME="${{ secrets.ACR_TEST_LOGIN_SERVER }}"
+ azd env set AZURE_ENV_ACR_NAME="$ACR_NAME"
+ echo "Set ACR name to: $ACR_NAME"
+ else
+ echo "Skipping ACR name configuration (using existing image)"
+ fi
+
+ if [[ "$INPUT_EXP" == "true" ]]; then
+ echo "β
EXP ENABLED - Setting EXP parameters..."
+
+ if [[ -n "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID" ]]; then
+ EXP_LOG_ANALYTICS_ID="$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID"
+ else
+ EXP_LOG_ANALYTICS_ID="${{ vars.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}"
+ fi
+
+ if [[ -n "$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" ]]; then
+ EXP_AI_PROJECT_ID="$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID"
+ else
+ EXP_AI_PROJECT_ID="${{ vars.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }}"
+ fi
+
+ echo "AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: $EXP_LOG_ANALYTICS_ID"
+ echo "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
+ echo "β EXP DISABLED - Skipping EXP parameters"
+ fi
+
+ azd up --no-prompt
+ echo "β
Deployment succeeded"
+
+ WEBAPP_URL=$(azd env get-value WEB_APP_URL)
+ echo "WEBAPP_URL=${WEBAPP_URL}" >> $GITHUB_ENV
+ echo "WEBAPP_URL=${WEBAPP_URL}" >> $GITHUB_OUTPUT
+
+ - name: Setup Python Environment for Post-Deployment
+ shell: bash
+ run: |
+ echo "Setting up Python environment..."
+ python3 --version
+ pip3 --version
+
+ # Upgrade pip
+ python3 -m pip install --upgrade pip
+
+ # - name: Enable Public Network Access for Data Upload (WAF Only)
+ # if: inputs.WAF_ENABLED == 'true'
+ # shell: bash
+ # env:
+ # INPUT_RESOURCE_GROUP_NAME: ${{ inputs.RESOURCE_GROUP_NAME }}
+ # INPUT_EXP: ${{ inputs.EXP }}
+ # INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }}
+ # run: |
+ # echo "π Temporarily enabling public network access for data upload..."
+ # SUBSCRIPTION_ID=$(az account show --query id -o tsv)
+ # SP_OBJECT_ID=$(az ad sp show --id ${{ secrets.AZURE_CLIENT_ID }} --query id -o tsv)
+
+ # # Get Cosmos DB account name and enable public access
+ # COSMOS_ACCOUNT=$(az cosmosdb list --resource-group "$INPUT_RESOURCE_GROUP_NAME" --query "[0].name" -o tsv)
+ # if [[ -n "$COSMOS_ACCOUNT" ]]; then
+ # echo "Enabling public access for Cosmos DB: $COSMOS_ACCOUNT"
+ # az cosmosdb update --name "$COSMOS_ACCOUNT" --resource-group "$INPUT_RESOURCE_GROUP_NAME" --public-network-access Enabled
+
+ # # Get GitHub Actions runner public IP and add to firewall
+ # RUNNER_IP=$(curl -s https://api.ipify.org)
+ # echo "GitHub Actions Runner IP: $RUNNER_IP"
+ # echo "Adding runner IP to Cosmos DB firewall rules..."
+ # az cosmosdb update --name "$COSMOS_ACCOUNT" --resource-group "$INPUT_RESOURCE_GROUP_NAME" \
+ # --ip-range-filter "$RUNNER_IP"
+ # echo "β
Cosmos DB public access enabled and runner IP added to firewall"
+ # else
+ # echo "β οΈ No Cosmos DB account found in resource group"
+ # fi
+
+ # # Get AI Services account - check current RG first, then extract from EXP project ID if needed
+ # AI_SERVICES_ACCOUNT=$(az cognitiveservices account list --resource-group "$INPUT_RESOURCE_GROUP_NAME" --query "[?kind=='AIServices'].name | [0]" -o tsv)
+ # AI_SERVICES_RG="$INPUT_RESOURCE_GROUP_NAME"
+
+ # # If EXP mode, extract AI Services account name directly from the project resource ID
+ # if [[ -z "$AI_SERVICES_ACCOUNT" && "$INPUT_EXP" == "true" ]]; then
+ # echo "EXP mode: Extracting AI Services from existing AI project resource ID..."
+ # if [[ -n "$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" ]]; then
+ # EXP_PROJECT_ID="$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID"
+ # else
+ # EXP_PROJECT_ID="${{ vars.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }}"
+ # fi
+ # # Extract resource group: .../resourceGroups/{rg}/...
+ # EXP_RG=$(echo "$EXP_PROJECT_ID" | sed -n 's|.*/resourceGroups/\([^/]*\)/.*|\1|p')
+ # # Extract account name: .../accounts/{accountName}/projects/...
+ # EXP_ACCOUNT=$(echo "$EXP_PROJECT_ID" | sed -n 's|.*/accounts/\([^/]*\)/.*|\1|p')
+ # if [[ -n "$EXP_ACCOUNT" && -n "$EXP_RG" ]]; then
+ # AI_SERVICES_ACCOUNT="$EXP_ACCOUNT"
+ # AI_SERVICES_RG="$EXP_RG"
+ # echo "Found AI Services from EXP project: $AI_SERVICES_ACCOUNT (RG: $AI_SERVICES_RG)"
+ # fi
+ # fi
+
+ # if [[ -n "$AI_SERVICES_ACCOUNT" ]]; then
+ # echo "Enabling public access for AI Services: $AI_SERVICES_ACCOUNT (RG: $AI_SERVICES_RG)"
+ # az rest --method PATCH \
+ # --url "https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${AI_SERVICES_RG}/providers/Microsoft.CognitiveServices/accounts/${AI_SERVICES_ACCOUNT}?api-version=2024-10-01" \
+ # --body '{"properties":{"publicNetworkAccess":"Enabled"}}'
+ # echo "β
AI Services public access enabled"
+
+ # # Assign Cognitive Services roles to service principal for embeddings and OpenAI access
+ # echo "Assigning Cognitive Services roles to service principal..."
+ # AI_SERVICES_ID=$(az cognitiveservices account show --name "$AI_SERVICES_ACCOUNT" --resource-group "$AI_SERVICES_RG" --query id -o tsv)
+ # az role assignment create --assignee-object-id "$SP_OBJECT_ID" --assignee-principal-type ServicePrincipal \
+ # --role "Cognitive Services OpenAI User" --scope "$AI_SERVICES_ID" 2>/dev/null || echo "OpenAI User role may already be assigned"
+ # az role assignment create --assignee-object-id "$SP_OBJECT_ID" --assignee-principal-type ServicePrincipal \
+ # --role "Cognitive Services User" --scope "$AI_SERVICES_ID" 2>/dev/null || echo "Cognitive Services User role may already be assigned"
+ # echo "β
Cognitive Services roles assigned"
+ # else
+ # echo "β οΈ No AI Services account found"
+ # fi
+
+ # # Get AI Search service and enable public access
+ # SEARCH_SERVICE=$(az search service list --resource-group "$INPUT_RESOURCE_GROUP_NAME" --query "[0].name" -o tsv)
+ # if [[ -n "$SEARCH_SERVICE" ]]; then
+ # echo "Enabling public access for AI Search: $SEARCH_SERVICE"
+ # az search service update --name "$SEARCH_SERVICE" --resource-group "$INPUT_RESOURCE_GROUP_NAME" --public-access enabled
+ # echo "β
AI Search public access enabled"
+ # else
+ # echo "β οΈ No AI Search service found in resource group"
+ # fi
+
+ # # Wait for network changes to propagate (5 minutes for Cosmos DB with private endpoints)
+ # echo "β³ Waiting 5 minutes for Cosmos DB network changes to propagate..."
+ # sleep 300
+
+ # # Verify Cosmos DB is accessible before proceeding
+ # if [[ -n "$COSMOS_ACCOUNT" ]]; then
+ # echo "π Verifying Cosmos DB connectivity..."
+ # COSMOS_ENDPOINT="https://${COSMOS_ACCOUNT}.documents.azure.com:443/"
+ # MAX_RETRIES=10
+ # RETRY_COUNT=0
+ # while [[ $RETRY_COUNT -lt $MAX_RETRIES ]]; do
+ # HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "${COSMOS_ENDPOINT}" --max-time 10 2>/dev/null || echo "000")
+ # if [[ "$HTTP_CODE" == "401" || "$HTTP_CODE" == "200" ]]; then
+ # echo "β
Cosmos DB is accessible (HTTP $HTTP_CODE - auth required, network OK)"
+ # break
+ # fi
+ # RETRY_COUNT=$((RETRY_COUNT + 1))
+ # echo "β³ Cosmos DB not yet accessible (HTTP $HTTP_CODE), retry $RETRY_COUNT/$MAX_RETRIES..."
+ # sleep 30
+ # done
+ # if [[ $RETRY_COUNT -eq $MAX_RETRIES ]]; then
+ # echo "β οΈ Cosmos DB connectivity check timed out, proceeding anyway..."
+ # fi
+ # fi
+ # echo "β
Network changes should now be propagated"
+
+ - name: Run Data Upload Scripts
+ shell: bash
+ run: |
+ echo "π Running data upload scripts to populate product catalogs and create search indexes..."
+
+ # Navigate to workspace root
+ cd $GITHUB_WORKSPACE
+
+ # Run the data upload script (it will pull parameters from azd env and install its own requirements)
+ bash ./infra/scripts/data_scripts/run_upload_data_scripts.sh
+
+ if [[ $? -eq 0 ]]; then
+ echo "β
Data upload completed successfully"
+ else
+ echo "β Data upload failed"
+ exit 1
+ fi
+ - name: Run Agent Creation Scripts
+ shell: bash
+ run: |
+ echo "π€ Running agent creation scripts to set up Azure AI Foundry agents..."
+
+ # Navigate to workspace root
+ cd $GITHUB_WORKSPACE
+
+ # Run the agent creation script (it will pull parameters from azd env and install its own requirements)
+ bash ./infra/scripts/agent_scripts/run_create_agents_scripts.sh
+
+ if [[ $? -eq 0 ]]; then
+ echo "β
Agent creation completed successfully"
+ else
+ echo "β Agent creation failed"
+ exit 1
+ fi
+
+ - name: Wait for Data Propagation
+ shell: bash
+ run: |
+ echo "β³ Waiting 5 minutes for data to propagate and indexes to update..."
+ sleep 300
+ echo "β
Wait completed, data should now be available"
+
+ - name: Wait for App Readiness (E2E Tests)
+ if: inputs.E2E_TEST == 'true' || (inputs.WAF_ENABLED == 'false' && inputs.EXP == 'false')
+ shell: bash
+ run: |
+ echo "β³ E2E tests or Golden Path deployment - Waiting 10 minutes for app to be fully ready..."
+ sleep 600
+ echo "β
App readiness wait completed"
+
+ # - name: Restore Private Network Access (WAF Only)
+ # if: always() && inputs.WAF_ENABLED == 'true'
+ # shell: bash
+ # env:
+ # INPUT_RESOURCE_GROUP_NAME: ${{ inputs.RESOURCE_GROUP_NAME }}
+ # INPUT_EXP: ${{ inputs.EXP }}
+ # INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }}
+ # run: |
+ # echo "π Restoring private network access settings..."
+ # SUBSCRIPTION_ID=$(az account show --query id -o tsv 2>/dev/null)
+
+ # # Get Cosmos DB account name
+ # COSMOS_ACCOUNT=$(az cosmosdb list --resource-group "$INPUT_RESOURCE_GROUP_NAME" --query "[0].name" -o tsv 2>/dev/null)
+ # if [[ -n "$COSMOS_ACCOUNT" ]]; then
+ # echo "Disabling public access for Cosmos DB: $COSMOS_ACCOUNT"
+ # # Clear IP rules first
+ # az cosmosdb update --name "$COSMOS_ACCOUNT" --resource-group "$INPUT_RESOURCE_GROUP_NAME" --ip-range-filter "" || true
+ # # Then disable public network access
+ # az cosmosdb update --name "$COSMOS_ACCOUNT" --resource-group "$INPUT_RESOURCE_GROUP_NAME" --public-network-access Disabled || true
+ # echo "β
Cosmos DB public access disabled and firewall rules cleared"
+ # fi
+
+ # # Get AI Services account - check current RG first, then EXP RG if needed
+ # AI_SERVICES_ACCOUNT=$(az cognitiveservices account list --resource-group "$INPUT_RESOURCE_GROUP_NAME" --query "[0].name" -o tsv 2>/dev/null)
+ # AI_SERVICES_RG="$INPUT_RESOURCE_GROUP_NAME"
+
+ # # If EXP mode and no AI Services in current RG, get from existing AI project RG
+ # if [[ -z "$AI_SERVICES_ACCOUNT" && "$INPUT_EXP" == "true" ]]; then
+ # if [[ -n "$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" ]]; then
+ # EXP_RG=$(echo "$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" | sed -n 's|.*/resourceGroups/\([^/]*\)/.*|\1|p')
+ # else
+ # EXP_RG=$(echo "${{ vars.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }}" | sed -n 's|.*/resourceGroups/\([^/]*\)/.*|\1|p')
+ # fi
+ # if [[ -n "$EXP_RG" ]]; then
+ # AI_SERVICES_ACCOUNT=$(az cognitiveservices account list --resource-group "$EXP_RG" --query "[0].name" -o tsv 2>/dev/null)
+ # AI_SERVICES_RG="$EXP_RG"
+ # fi
+ # fi
+
+ # if [[ -n "$AI_SERVICES_ACCOUNT" ]]; then
+ # echo "Disabling public access for AI Services: $AI_SERVICES_ACCOUNT (RG: $AI_SERVICES_RG)"
+ # az rest --method PATCH \
+ # --url "https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${AI_SERVICES_RG}/providers/Microsoft.CognitiveServices/accounts/${AI_SERVICES_ACCOUNT}?api-version=2024-10-01" \
+ # --body '{"properties":{"publicNetworkAccess":"Disabled"}}' || true
+ # echo "β
AI Services public access disabled"
+ # fi
+
+ # # Get AI Search service and disable public access
+ # SEARCH_SERVICE=$(az search service list --resource-group "$INPUT_RESOURCE_GROUP_NAME" --query "[0].name" -o tsv 2>/dev/null)
+ # if [[ -n "$SEARCH_SERVICE" ]]; then
+ # echo "Disabling public access for AI Search: $SEARCH_SERVICE"
+ # az search service update --name "$SEARCH_SERVICE" --resource-group "$INPUT_RESOURCE_GROUP_NAME" --public-access disabled || true
+ # echo "β
AI Search public access disabled"
+ # fi
+
+ # echo "π Private network access restored"
+
+ - name: Generate Deployment Summary
+ if: always()
+ shell: bash
+ env:
+ 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_IMAGE_TAG: ${{ inputs.IMAGE_TAG }}
+ INPUT_WAF_ENABLED: ${{ inputs.WAF_ENABLED }}
+ INPUT_EXP: ${{ inputs.EXP }}
+ CONFIG_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' }}
+ run: |
+ echo "## π Deploy Job Summary (Linux)" >> $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** | \`$INPUT_RESOURCE_GROUP_NAME \` |" >> $GITHUB_STEP_SUMMARY
+ echo "| **Configuration Type** | \`$CONFIG_TYPE\` |" >> $GITHUB_STEP_SUMMARY
+ echo "| **Azure Region (Infrastructure)** | \`$INPUT_AZURE_LOCATION\` |" >> $GITHUB_STEP_SUMMARY
+ echo "| **Azure OpenAI Region** | \`$INPUT_AZURE_ENV_OPENAI_LOCATION\` |" >> $GITHUB_STEP_SUMMARY
+ echo "| **Docker Image Tag** | \`$INPUT_IMAGE_TAG\` |" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ if [[ "${{ job.status }}" == "success" ]]; then
+ echo "### β
Deployment Details" >> $GITHUB_STEP_SUMMARY
+ echo "- **Container Web App URL**: [${{ steps.get_output_linux.outputs.WEBAPP_URL }}](${{ steps.get_output_linux.outputs.WEBAPP_URL }})" >> $GITHUB_STEP_SUMMARY
+ echo "- Successfully deployed to Azure with all resources configured" >> $GITHUB_STEP_SUMMARY
+ else
+ echo "### β Deployment Failed" >> $GITHUB_STEP_SUMMARY
+ echo "- Deployment process encountered an error" >> $GITHUB_STEP_SUMMARY
+ echo "- Check the deployment steps above 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-windows.yml b/.github/workflows/job-deploy-windows.yml
new file mode 100644
index 00000000..00a5993b
--- /dev/null
+++ b/.github/workflows/job-deploy-windows.yml
@@ -0,0 +1,553 @@
+name: Deploy Steps - Windows
+
+permissions:
+ contents: read
+ actions: read
+
+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
+ E2E_TEST:
+ required: false
+ type: string
+ default: 'false'
+ outputs:
+ CONTAINER_WEB_APPURL:
+ description: "Container Web App URL"
+ value: ${{ jobs.deploy-windows.outputs.WEBAPP_URL }}
+
+jobs:
+ deploy-windows:
+ runs-on: windows-latest
+ env:
+ AZURE_DEV_COLLECT_TELEMETRY: ${{ vars.AZURE_DEV_COLLECT_TELEMETRY }}
+ outputs:
+ WEBAPP_URL: ${{ steps.get_output_windows.outputs.WEBAPP_URL }}
+ steps:
+ - name: Checkout Code
+ uses: actions/checkout@v4
+
+ - 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_BUILD_DOCKER_IMAGE: ${{ inputs.BUILD_DOCKER_IMAGE }}
+ INPUT_EXP: ${{ inputs.EXP }}
+ INPUT_WAF_ENABLED: ${{ inputs.WAF_ENABLED }}
+ INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}
+ INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }}
+ run: |
+ echo "π Validating workflow input parameters..."
+ VALIDATION_FAILED=false
+
+ # Validate ENV_NAME (required, alphanumeric and hyphens)
+ if [[ -z "$INPUT_ENV_NAME" ]]; then
+ echo "β ERROR: ENV_NAME is required but 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 alphanumerics 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 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
+ 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 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
+ else
+ echo "β
AZURE_LOCATION: '$INPUT_AZURE_LOCATION' is valid"
+ fi
+
+ # Validate RESOURCE_GROUP_NAME (required, Azure naming convention)
+ if [[ -z "$INPUT_RESOURCE_GROUP_NAME" ]]; then
+ echo "β ERROR: RESOURCE_GROUP_NAME is required but 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 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, max 128 characters"
+ VALIDATION_FAILED=true
+ else
+ echo "β
IMAGE_TAG: '$INPUT_IMAGE_TAG' is valid"
+ fi
+
+ # Validate BUILD_DOCKER_IMAGE (required, must be 'true' or 'false')
+ if [[ "$INPUT_BUILD_DOCKER_IMAGE" != "true" && "$INPUT_BUILD_DOCKER_IMAGE" != "false" ]]; then
+ echo "β ERROR: BUILD_DOCKER_IMAGE must be 'true' or 'false', got: '$INPUT_BUILD_DOCKER_IMAGE'"
+ VALIDATION_FAILED=true
+ else
+ echo "β
BUILD_DOCKER_IMAGE: '$INPUT_BUILD_DOCKER_IMAGE' is valid"
+ fi
+
+ # Validate EXP (required, must be 'true' or 'false')
+ 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 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, if provided must be valid Resource ID)
+ 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/workspaces/[^/]+$ ]]; 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 AZURE_EXISTING_AI_PROJECT_RESOURCE_ID (optional, if provided must be valid Resource ID)
+ if [[ -n "$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" ]]; then
+ if [[ ! "$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" =~ ^/subscriptions/[a-fA-F0-9-]+/resourceGroups/[^/]+/providers/(Microsoft\.MachineLearningServices/(workspaces|projects)/[^/]+|Microsoft\.CognitiveServices/accounts/[^/]+/projects/[^/]+)$ ]]; then
+ echo "β ERROR: AZURE_EXISTING_AI_PROJECT_RESOURCE_ID is invalid. Must be a valid Azure Resource ID format:"
+ echo " /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.CognitiveServices/accounts/{accountName}/projects/{projectName}"
+ echo " Got: '$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID'"
+ VALIDATION_FAILED=true
+ else
+ echo "β
AZURE_EXISTING_AI_PROJECT_RESOURCE_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: Configure Parameters Based on WAF Setting
+ shell: bash
+ env:
+ INPUT_WAF_ENABLED: ${{ inputs.WAF_ENABLED }}
+ run: |
+ 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
+ echo "π§ Configuring Non-WAF deployment - using default main.parameters.json..."
+ fi
+
+ - name: Install azd
+ 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
+ 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_BUILD_DOCKER_IMAGE: ${{ inputs.BUILD_DOCKER_IMAGE }}
+ INPUT_EXP: ${{ inputs.EXP }}
+ INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}
+ INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }}
+ run: |
+ $ErrorActionPreference = "Stop"
+ Write-Host "Starting azd deployment..."
+ Write-Host "EXP: $env:INPUT_EXP"
+ Write-Host "Using Docker Image Tag: $env:INPUT_IMAGE_TAG"
+
+ Write-Host "Creating environment..."
+ azd env new "$env:INPUT_ENV_NAME" --no-prompt
+ Write-Host "Environment created: $env:INPUT_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_AI_SERVICE_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_IMAGETAG="$env:INPUT_IMAGE_TAG"
+
+ # Set ACR name only when building Docker image
+ if ("$env:INPUT_BUILD_DOCKER_IMAGE" -eq "true") {
+ $ACR_NAME = "${{ secrets.ACR_TEST_LOGIN_SERVER }}"
+ 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 ("$env:INPUT_EXP" -eq "true") {
+ Write-Host "EXP ENABLED β
- Setting EXP parameters..."
+
+ # Set EXP variables dynamically
+ 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 = "${{ vars.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}"
+ }
+
+ if ("$env:INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" -ne "") {
+ $EXP_AI_PROJECT_ID = "$env:INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID"
+ } else {
+ $EXP_AI_PROJECT_ID = "${{ vars.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."
+
+ $WEBAPP_URL = azd env get-value WEB_APP_URL
+ "WEBAPP_URL=$WEBAPP_URL" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
+ "WEBAPP_URL=$WEBAPP_URL" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
+
+ # - name: Enable Public Network Access for Data Upload (WAF Only)
+ # if: inputs.WAF_ENABLED == 'true'
+ # shell: bash
+ # env:
+ # INPUT_RESOURCE_GROUP_NAME: ${{ inputs.RESOURCE_GROUP_NAME }}
+ # INPUT_EXP: ${{ inputs.EXP }}
+ # INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }}
+ # run: |
+ # echo "π Temporarily enabling public network access for data upload..."
+ # SUBSCRIPTION_ID=$(az account show --query id -o tsv)
+ # SP_OBJECT_ID=$(az ad sp show --id ${{ secrets.AZURE_CLIENT_ID }} --query id -o tsv)
+
+ # # Get Cosmos DB account name and enable public access
+ # COSMOS_ACCOUNT=$(az cosmosdb list --resource-group "$INPUT_RESOURCE_GROUP_NAME" --query "[0].name" -o tsv)
+ # if [[ -n "$COSMOS_ACCOUNT" ]]; then
+ # echo "Enabling public access for Cosmos DB: $COSMOS_ACCOUNT"
+ # az cosmosdb update --name "$COSMOS_ACCOUNT" --resource-group "$INPUT_RESOURCE_GROUP_NAME" --public-network-access Enabled
+
+ # # Get GitHub Actions runner public IP and add to firewall
+ # RUNNER_IP=$(curl -s https://api.ipify.org)
+ # echo "GitHub Actions Runner IP: $RUNNER_IP"
+ # echo "Adding runner IP to Cosmos DB firewall rules..."
+ # az cosmosdb update --name "$COSMOS_ACCOUNT" --resource-group "$INPUT_RESOURCE_GROUP_NAME" \
+ # --ip-range-filter "$RUNNER_IP"
+ # echo "β
Cosmos DB public access enabled and runner IP added to firewall"
+ # else
+ # echo "β οΈ No Cosmos DB account found in resource group"
+ # fi
+
+ # # Get AI Services account - check current RG first, then extract from EXP project ID if needed
+ # AI_SERVICES_ACCOUNT=$(az cognitiveservices account list --resource-group "$INPUT_RESOURCE_GROUP_NAME" --query "[?kind=='AIServices'].name | [0]" -o tsv)
+ # AI_SERVICES_RG="$INPUT_RESOURCE_GROUP_NAME"
+
+ # # If EXP mode, extract AI Services account name directly from the project resource ID
+ # if [[ -z "$AI_SERVICES_ACCOUNT" && "$INPUT_EXP" == "true" ]]; then
+ # echo "EXP mode: Extracting AI Services from existing AI project resource ID..."
+ # if [[ -n "$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" ]]; then
+ # EXP_PROJECT_ID="$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID"
+ # else
+ # EXP_PROJECT_ID="${{ vars.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }}"
+ # fi
+ # # Extract resource group: .../resourceGroups/{rg}/...
+ # EXP_RG=$(echo "$EXP_PROJECT_ID" | sed -n 's|.*/resourceGroups/\([^/]*\)/.*|\1|p')
+ # # Extract account name: .../accounts/{accountName}/projects/...
+ # EXP_ACCOUNT=$(echo "$EXP_PROJECT_ID" | sed -n 's|.*/accounts/\([^/]*\)/.*|\1|p')
+ # if [[ -n "$EXP_ACCOUNT" && -n "$EXP_RG" ]]; then
+ # AI_SERVICES_ACCOUNT="$EXP_ACCOUNT"
+ # AI_SERVICES_RG="$EXP_RG"
+ # echo "Found AI Services from EXP project: $AI_SERVICES_ACCOUNT (RG: $AI_SERVICES_RG)"
+ # fi
+ # fi
+
+ # if [[ -n "$AI_SERVICES_ACCOUNT" ]]; then
+ # echo "Enabling public access for AI Services: $AI_SERVICES_ACCOUNT (RG: $AI_SERVICES_RG)"
+ # az rest --method PATCH \
+ # --url "https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${AI_SERVICES_RG}/providers/Microsoft.CognitiveServices/accounts/${AI_SERVICES_ACCOUNT}?api-version=2024-10-01" \
+ # --body '{"properties":{"publicNetworkAccess":"Enabled"}}'
+ # echo "β
AI Services public access enabled"
+
+ # # Assign Cognitive Services roles to service principal for embeddings and OpenAI access
+ # echo "Assigning Cognitive Services roles to service principal..."
+ # AI_SERVICES_ID=$(az cognitiveservices account show --name "$AI_SERVICES_ACCOUNT" --resource-group "$AI_SERVICES_RG" --query id -o tsv)
+ # az role assignment create --assignee-object-id "$SP_OBJECT_ID" --assignee-principal-type ServicePrincipal \
+ # --role "Cognitive Services OpenAI User" --scope "$AI_SERVICES_ID" 2>/dev/null || echo "OpenAI User role may already be assigned"
+ # az role assignment create --assignee-object-id "$SP_OBJECT_ID" --assignee-principal-type ServicePrincipal \
+ # --role "Cognitive Services User" --scope "$AI_SERVICES_ID" 2>/dev/null || echo "Cognitive Services User role may already be assigned"
+ # echo "β
Cognitive Services roles assigned"
+ # else
+ # echo "β οΈ No AI Services account found"
+ # fi
+
+ # # Get AI Search service and enable public access
+ # SEARCH_SERVICE=$(az search service list --resource-group "$INPUT_RESOURCE_GROUP_NAME" --query "[0].name" -o tsv)
+ # if [[ -n "$SEARCH_SERVICE" ]]; then
+ # echo "Enabling public access for AI Search: $SEARCH_SERVICE"
+ # az search service update --name "$SEARCH_SERVICE" --resource-group "$INPUT_RESOURCE_GROUP_NAME" --public-access enabled
+ # echo "β
AI Search public access enabled"
+ # else
+ # echo "β οΈ No AI Search service found in resource group"
+ # fi
+
+ # # Wait for network changes to propagate (5 minutes for Cosmos DB with private endpoints)
+ # echo "β³ Waiting 5 minutes for Cosmos DB network changes to propagate..."
+ # sleep 300
+
+ # # Verify Cosmos DB is accessible before proceeding
+ # if [[ -n "$COSMOS_ACCOUNT" ]]; then
+ # echo "π Verifying Cosmos DB connectivity..."
+ # COSMOS_ENDPOINT="https://${COSMOS_ACCOUNT}.documents.azure.com:443/"
+ # MAX_RETRIES=10
+ # RETRY_COUNT=0
+ # while [[ $RETRY_COUNT -lt $MAX_RETRIES ]]; do
+ # HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "${COSMOS_ENDPOINT}" --max-time 10 2>/dev/null || echo "000")
+ # if [[ "$HTTP_CODE" == "401" || "$HTTP_CODE" == "200" ]]; then
+ # echo "β
Cosmos DB is accessible (HTTP $HTTP_CODE - auth required, network OK)"
+ # break
+ # fi
+ # RETRY_COUNT=$((RETRY_COUNT + 1))
+ # echo "β³ Cosmos DB not yet accessible (HTTP $HTTP_CODE), retry $RETRY_COUNT/$MAX_RETRIES..."
+ # sleep 30
+ # done
+ # if [[ $RETRY_COUNT -eq $MAX_RETRIES ]]; then
+ # echo "β οΈ Cosmos DB connectivity check timed out, proceeding anyway..."
+ # fi
+ # fi
+ # echo "β
Network changes should now be propagated"
+
+ - name: Run Post-Deployment Data Upload Scripts
+ shell: pwsh
+ run: |
+ Write-Host "Running data upload scripts..."
+
+ # Get service principal object ID for role assignments
+ $SP_OBJECT_ID = az ad sp show --id ${{ secrets.AZURE_CLIENT_ID }} --query id -o tsv
+ Write-Host "Service Principal Object ID: $SP_OBJECT_ID"
+
+ # Export for scripts to use
+ $env:AZURE_PRINCIPAL_ID = $SP_OBJECT_ID
+ $env:AZURE_CLIENT_ID = "${{ secrets.AZURE_CLIENT_ID }}"
+
+ bash ./infra/scripts/data_scripts/run_upload_data_scripts.sh
+ Write-Host "β
Data upload completed successfully"
+ - name: Setup Python 3.11 for Agent Scripts
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.11'
+
+ - name: Run Post-Deployment Agent Creation Scripts
+ shell: pwsh
+ run: |
+ Write-Host "Running agent creation scripts..."
+
+ # Get service principal object ID for role assignments
+ $SP_OBJECT_ID = az ad sp show --id ${{ secrets.AZURE_CLIENT_ID }} --query id -o tsv
+ Write-Host "Service Principal Object ID: $SP_OBJECT_ID"
+
+ # Export for scripts to use
+ $env:AZURE_PRINCIPAL_ID = $SP_OBJECT_ID
+ $env:AZURE_CLIENT_ID = "${{ secrets.AZURE_CLIENT_ID }}"
+
+ bash ./infra/scripts/agent_scripts/run_create_agents_scripts.sh
+
+ - name: Wait for Data Propagation
+ shell: bash
+ run: |
+ echo "β³ Waiting 5 minutes for data to propagate and indexes to update..."
+ sleep 300
+ echo "β
Wait completed, data should now be available"
+
+ - name: Wait for App Readiness (E2E Tests)
+ if: inputs.E2E_TEST == 'true' || (inputs.WAF_ENABLED == 'false' && inputs.EXP == 'false')
+ shell: bash
+ run: |
+ echo "β³ E2E tests or Golden Path deployment - Waiting 10 minutes for app to be fully ready..."
+ sleep 600
+ echo "β
App readiness wait completed"
+
+ # - name: Restore Private Network Access (WAF Only)
+ # if: always() && inputs.WAF_ENABLED == 'true'
+ # shell: bash
+ # env:
+ # INPUT_RESOURCE_GROUP_NAME: ${{ inputs.RESOURCE_GROUP_NAME }}
+ # INPUT_EXP: ${{ inputs.EXP }}
+ # INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }}
+ # run: |
+ # echo "π Restoring private network access settings..."
+ # SUBSCRIPTION_ID=$(az account show --query id -o tsv 2>/dev/null)
+
+ # # Get Cosmos DB account name
+ # COSMOS_ACCOUNT=$(az cosmosdb list --resource-group "$INPUT_RESOURCE_GROUP_NAME" --query "[0].name" -o tsv 2>/dev/null)
+ # if [[ -n "$COSMOS_ACCOUNT" ]]; then
+ # echo "Disabling public access for Cosmos DB: $COSMOS_ACCOUNT"
+ # # Clear IP rules first
+ # az cosmosdb update --name "$COSMOS_ACCOUNT" --resource-group "$INPUT_RESOURCE_GROUP_NAME" --ip-range-filter "" || true
+ # # Then disable public network access
+ # az cosmosdb update --name "$COSMOS_ACCOUNT" --resource-group "$INPUT_RESOURCE_GROUP_NAME" --public-network-access Disabled || true
+ # echo "β
Cosmos DB public access disabled and firewall rules cleared"
+ # fi
+
+ # # Get AI Services account - check current RG first, then EXP RG if needed
+ # AI_SERVICES_ACCOUNT=$(az cognitiveservices account list --resource-group "$INPUT_RESOURCE_GROUP_NAME" --query "[0].name" -o tsv 2>/dev/null)
+ # AI_SERVICES_RG="$INPUT_RESOURCE_GROUP_NAME"
+
+ # # If EXP mode and no AI Services in current RG, get from existing AI project RG
+ # if [[ -z "$AI_SERVICES_ACCOUNT" && "$INPUT_EXP" == "true" ]]; then
+ # if [[ -n "$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" ]]; then
+ # EXP_RG=$(echo "$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" | sed -n 's|.*/resourceGroups/\([^/]*\)/.*|\1|p')
+ # else
+ # EXP_RG=$(echo "${{ vars.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }}" | sed -n 's|.*/resourceGroups/\([^/]*\)/.*|\1|p')
+ # fi
+ # if [[ -n "$EXP_RG" ]]; then
+ # AI_SERVICES_ACCOUNT=$(az cognitiveservices account list --resource-group "$EXP_RG" --query "[0].name" -o tsv 2>/dev/null)
+ # AI_SERVICES_RG="$EXP_RG"
+ # fi
+ # fi
+
+ # if [[ -n "$AI_SERVICES_ACCOUNT" ]]; then
+ # echo "Disabling public access for AI Services: $AI_SERVICES_ACCOUNT (RG: $AI_SERVICES_RG)"
+ # az rest --method PATCH \
+ # --url "https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${AI_SERVICES_RG}/providers/Microsoft.CognitiveServices/accounts/${AI_SERVICES_ACCOUNT}?api-version=2024-10-01" \
+ # --body '{"properties":{"publicNetworkAccess":"Disabled"}}' || true
+ # echo "β
AI Services public access disabled"
+ # fi
+
+ # # Get AI Search service and disable public access
+ # SEARCH_SERVICE=$(az search service list --resource-group "$INPUT_RESOURCE_GROUP_NAME" --query "[0].name" -o tsv 2>/dev/null)
+ # if [[ -n "$SEARCH_SERVICE" ]]; then
+ # echo "Disabling public access for AI Search: $SEARCH_SERVICE"
+ # az search service update --name "$SEARCH_SERVICE" --resource-group "$INPUT_RESOURCE_GROUP_NAME" --public-access disabled || true
+ # echo "β
AI Search public access disabled"
+ # fi
+
+ # echo "π Private network access restored"
+
+ - name: Generate Deployment Summary
+ if: always()
+ shell: bash
+ env:
+ 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_IMAGE_TAG: ${{ inputs.IMAGE_TAG }}
+ INPUT_WAF_ENABLED: ${{ inputs.WAF_ENABLED }}
+ INPUT_EXP: ${{ inputs.EXP }}
+ JOB_STATUS: ${{ job.status }}
+ 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
+ CONFIG_TYPE="Non-WAF + Non-EXP"
+ if [[ "$INPUT_WAF_ENABLED" == "true" && "$INPUT_EXP" == "true" ]]; then
+ CONFIG_TYPE="WAF + EXP"
+ elif [[ "$INPUT_WAF_ENABLED" == "true" && "$INPUT_EXP" != "true" ]]; then
+ CONFIG_TYPE="WAF + Non-EXP"
+ elif [[ "$INPUT_WAF_ENABLED" != "true" && "$INPUT_EXP" == "true" ]]; then
+ CONFIG_TYPE="Non-WAF + EXP"
+ fi
+ echo "| **Configuration Type** | \`$CONFIG_TYPE\` |" >> $GITHUB_STEP_SUMMARY
+ echo "| **Resource Group** | \`$INPUT_RESOURCE_GROUP_NAME\` |" >> $GITHUB_STEP_SUMMARY
+ echo "| **Azure Region (Infrastructure)** | \`$INPUT_AZURE_LOCATION\` |" >> $GITHUB_STEP_SUMMARY
+ echo "| **Azure OpenAI Region** | \`$INPUT_AZURE_ENV_OPENAI_LOCATION\` |" >> $GITHUB_STEP_SUMMARY
+ echo "| **Docker Image Tag** | \`$INPUT_IMAGE_TAG\` |" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ if [ "$JOB_STATUS" == "success" ]; then
+ echo "### β
Deployment Details" >> $GITHUB_STEP_SUMMARY
+ echo "- **Container Web App URL**: [${{ steps.get_output_windows.outputs.WEBAPP_URL }}](${{ steps.get_output_windows.outputs.WEBAPP_URL }})" >> $GITHUB_STEP_SUMMARY
+ echo "- Successfully deployed to Azure with all resources configured" >> $GITHUB_STEP_SUMMARY
+ echo "- Schemas registered and sample data uploaded successfully" >> $GITHUB_STEP_SUMMARY
+ else
+ echo "### β Deployment Failed" >> $GITHUB_STEP_SUMMARY
+ echo "- Deployment process encountered an error" >> $GITHUB_STEP_SUMMARY
+ echo "- Check the deployment steps above 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..8d661b08
--- /dev/null
+++ b/.github/workflows/job-deploy.yml
@@ -0,0 +1,497 @@
+name: Deploy Job
+
+permissions:
+ contents: read
+ actions: read
+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 }}
+ CONTAINER_WEB_APPURL:
+ description: "Container Web App URL"
+ value: ${{ jobs.deploy-linux.outputs.CONTAINER_WEB_APPURL || jobs.deploy-windows.outputs.CONTAINER_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
+ 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 Workflow Input Parameters
+ shell: bash
+ env:
+ INPUT_AZURE_LOCATION: ${{ inputs.azure_location }}
+ INPUT_RESOURCE_GROUP_NAME: ${{ inputs.resource_group_name }}
+ INPUT_EXP: ${{ inputs.EXP }}
+ INPUT_WAF_ENABLED: ${{ inputs.waf_enabled }}
+ 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_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }}
+ INPUT_DOCKER_IMAGE_TAG: ${{ inputs.docker_image_tag }}
+ run: |
+ echo "π Validating workflow input parameters..."
+ VALIDATION_FAILED=false
+
+ # 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/workspaces/[^/]+$ ]]; 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 AZURE_EXISTING_AI_PROJECT_RESOURCE_ID (Azure Resource ID format)
+ if [[ -n "$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" ]]; then
+ if [[ ! "$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" =~ ^/subscriptions/[a-fA-F0-9-]+/resourceGroups/[^/]+/providers/(Microsoft\.MachineLearningServices/(workspaces|projects)/[^/]+|Microsoft\.CognitiveServices/accounts/[^/]+/projects/[^/]+)$ ]]; then
+ echo "β ERROR: AZURE_EXISTING_AI_PROJECT_RESOURCE_ID is invalid. Must be a valid Azure Resource ID format:"
+ echo " /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.CognitiveServices/accounts/{accountName}/projects/{projectName}"
+ echo " Got: '$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID'"
+ VALIDATION_FAILED=true
+ else
+ echo "β
AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: Valid Resource ID format"
+ fi
+ fi
+
+ # Validate docker_image_tag (Docker tag pattern)
+ if [[ -n "$INPUT_DOCKER_IMAGE_TAG" ]]; then
+ # Docker tags: lowercase and uppercase letters, digits, underscores, periods, and hyphens
+ # Cannot start with period or hyphen, max 128 characters
+ if [[ ! "$INPUT_DOCKER_IMAGE_TAG" =~ ^[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}$ ]]; then
+ echo "β ERROR: docker_image_tag '$INPUT_DOCKER_IMAGE_TAG' is invalid. Must:"
+ echo " - Start with alphanumeric or underscore"
+ echo " - Contain only alphanumerics, underscores, periods, hyphens"
+ echo " - Be max 128 characters"
+ VALIDATION_FAILED=true
+ else
+ echo "β
docker_image_tag: '$INPUT_DOCKER_IMAGE_TAG' 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
+ shell: bash
+ env:
+ INPUT_EXP: ${{ inputs.EXP }}
+ INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}
+ INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }}
+ run: |
+ echo "π Validating EXP configuration..."
+
+ if [[ "$INPUT_EXP" != "true" ]]; then
+ if [[ -n "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID" ]] || [[ -n "$INPUT_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 "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID" ]] && echo " - Azure Log Analytics Workspace ID: '$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID'"
+ [[ -n "$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" ]] && echo " - Azure AI Project Resource ID: '$INPUT_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
+ env:
+ AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
+ AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
+ AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
+ AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ AZURE_REGIONS: ${{ vars.AZURE_REGIONS }}
+ GPT_MIN_CAPACITY: ${{ env.GPT_MIN_CAPACITY }}
+ run: |
+
+ chmod +x infra/scripts/checkquota.sh
+ if ! infra/scripts/checkquota.sh; then
+ # If quota check fails due to insufficient quota, set the flag
+ if grep -q "No region with sufficient quota found" infra/scripts/checkquota.sh; then
+ echo "QUOTA_FAILED=true" >> $GITHUB_ENV
+ fi
+ exit 1 # Fail the pipeline if any other failure occurs
+ 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
+ env:
+ INPUT_TRIGGER_TYPE: ${{ inputs.trigger_type }}
+ INPUT_AZURE_LOCATION: ${{ inputs.azure_location }}
+ 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 [[ "$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
+ 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
+ env:
+ INPUT_RESOURCE_GROUP_NAME: ${{ inputs.resource_group_name }}
+ run: |
+ # Check if a resource group name was provided as input
+ 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="byocc" # 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
+ shell: bash
+ env:
+ INPUT_DOCKER_IMAGE_TAG: ${{ inputs.docker_image_tag }}
+ run: |
+ if [[ "${{ env.BUILD_DOCKER_IMAGE }}" == "true" ]]; then
+ if [[ -n "$INPUT_DOCKER_IMAGE_TAG" ]]; then
+ IMAGE_TAG="$INPUT_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"
+
+ if [[ "$BRANCH_NAME" == "main" ]]; then
+ IMAGE_TAG="latest"
+ elif [[ "$BRANCH_NAME" == "dev" ]]; then
+ IMAGE_TAG="dev"
+ elif [[ "$BRANCH_NAME" == "demo" ]]; then
+ IMAGE_TAG="demo"
+ else
+ IMAGE_TAG="latest"
+ echo "Using default for branch '$BRANCH_NAME' - image tag: latest"
+ 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
+ env:
+ INPUT_TRIGGER_TYPE: ${{ inputs.trigger_type }}
+ INPUT_AZURE_LOCATION: ${{ inputs.azure_location }}
+ INPUT_RESOURCE_GROUP_NAME: ${{ inputs.resource_group_name }}
+ run: |
+ echo "## π Workflow Configuration Summary" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "| Configuration | Value |" >> $GITHUB_STEP_SUMMARY
+ echo "|---------------|-------|" >> $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
+ 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 [[ "$INPUT_TRIGGER_TYPE" == "workflow_dispatch" && -n "$INPUT_AZURE_LOCATION" ]]; then
+ echo "| **Azure Location** | \`$INPUT_AZURE_LOCATION\` (User Selected) |" >> $GITHUB_STEP_SUMMARY
+ fi
+
+ if [[ -n "$INPUT_RESOURCE_GROUP_NAME" ]]; then
+ echo "| **Resource Group** | \`$INPUT_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 [[ "$INPUT_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' && !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 }}
+ 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' && !cancelled() && 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..0cb58768
--- /dev/null
+++ b/.github/workflows/job-docker-build.yml
@@ -0,0 +1,135 @@
+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: Get current date
+ id: date
+ run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
+
+ - name: Determine Tag Name Based on Branch
+ id: determine_tag
+ run: |
+ BRANCH_NAME="${{ github.ref_name }}"
+ if [[ "$BRANCH_NAME" == "main" ]]; then
+ echo "tagname=latest_waf" >> $GITHUB_OUTPUT
+ elif [[ "$BRANCH_NAME" == "dev" ]]; then
+ echo "tagname=dev" >> $GITHUB_OUTPUT
+ elif [[ "$BRANCH_NAME" == "demo" ]]; then
+ echo "tagname=demo" >> $GITHUB_OUTPUT
+ elif [[ "$BRANCH_NAME" == "dependabotchanges" ]]; then
+ echo "tagname=dependabotchanges" >> $GITHUB_OUTPUT
+ elif [[ "$BRANCH_NAME" == "PSL-US-27776" ]]; then
+ echo "tagname=prerelease" >> $GITHUB_OUTPUT
+ else
+ # Clean branch name for use as Docker tag
+ CLEAN_BRANCH_NAME=$(echo "$BRANCH_NAME" | sed 's/[^a-zA-Z0-9._-]/-/g' | sed 's/--*/-/g' | sed 's/^-\|-$//g')
+ echo "tagname=$CLEAN_BRANCH_NAME" >> $GITHUB_OUTPUT
+ fi
+ - name: Generate Unique Docker Image Tag
+ id: generate_docker_tag
+ shell: bash
+ run: |
+ echo "π¨ Building new Docker image - generating unique tag..."
+ 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}-${{ steps.date.outputs.date }}-${{ github.run_number }}"
+ 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_LOGIN_SERVER }}
+ username: ${{ secrets.ACR_USERNAME }}
+ password: ${{ secrets.ACR_PASSWORD }}
+
+ - name: Output ACR Login Server
+ run: |
+ echo "ACR Login Server: ${{ secrets.ACR_LOGIN_SERVER }}"
+ - name: Build and Push Docker Image for WebApp
+ uses: docker/build-push-action@v6
+ env:
+ DOCKER_BUILD_SUMMARY: false
+ with:
+ context: ./src/App
+ file: ./src/App/Dockerfile
+ push: true
+ tags: |
+ ${{ secrets.ACR_LOGIN_SERVER || 'acrlogin.azurecr.io' }}/frontend:${{ steps.determine_tag.outputs.tagname }}
+ ${{ secrets.ACR_LOGIN_SERVER || 'acrlogin.azurecr.io' }}/frontend:${{ steps.generate_docker_tag.outputs.IMAGE_TAG }}
+ - name: Build and Push Docker Image for API
+ uses: docker/build-push-action@v6
+ env:
+ DOCKER_BUILD_SUMMARY: false
+ with:
+ context: ./src/api
+ file: ./src/api/Dockerfile
+ push: true
+ tags: |
+ ${{ secrets.ACR_LOGIN_SERVER || 'acrlogin.azurecr.io' }}/backend:${{ steps.determine_tag.outputs.tagname }}
+ ${{ secrets.ACR_LOGIN_SERVER || 'acrlogin.azurecr.io' }}/backend:${{ steps.generate_docker_tag.outputs.IMAGE_TAG }}
+ - 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_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 Tag** | \`${{ steps.determine_tag.outputs.tagname }}\` |" >> $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 two Docker images to ACR:" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "**Built Images:**" >> $GITHUB_STEP_SUMMARY
+ echo "- \`${ACR_NAME}/frontend:${{ steps.determine_tag.outputs.tagname }}\`" >> $GITHUB_STEP_SUMMARY
+ echo "- \`${ACR_NAME}/frontend:${{ steps.generate_docker_tag.outputs.IMAGE_TAG }}\`" >> $GITHUB_STEP_SUMMARY
+ echo "- \`${ACR_NAME}/backend:${{ steps.determine_tag.outputs.tagname }}\`" >> $GITHUB_STEP_SUMMARY
+ echo "- \`${ACR_NAME}/backend:${{ 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
\ No newline at end of file
diff --git a/.github/workflows/job-send-notification.yml b/.github/workflows/job-send-notification.yml
new file mode 100644
index 00000000..5821f970
--- /dev/null
+++ b/.github/workflows/job-send-notification.yml
@@ -0,0 +1,436 @@
+name: Send Notification Job
+permissions:
+ contents: read
+ actions: read
+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: false
+ default: ''
+ type: string
+ CONTAINER_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: "customer-chatbot-solution-accelerator"
+ steps:
+ - name: Validate Workflow Input Parameters
+ shell: bash
+ env:
+ INPUT_TRIGGER_TYPE: ${{ inputs.trigger_type }}
+ INPUT_WAF_ENABLED: ${{ inputs.waf_enabled }}
+ INPUT_EXP: ${{ inputs.EXP }}
+ # INPUT_RUN_E2E_TESTS: ${{ inputs.run_e2e_tests }}
+ INPUT_EXISTING_WEBAPP_URL: ${{ inputs.existing_webapp_url }}
+ INPUT_DEPLOY_RESULT: ${{ inputs.deploy_result }}
+ # INPUT_E2E_TEST_RESULT: ${{ inputs.e2e_test_result }}
+ INPUT_CONTAINER_WEB_APPURL: ${{ inputs.CONTAINER_WEB_APPURL }}
+ INPUT_RESOURCE_GROUP_NAME: ${{ inputs.RESOURCE_GROUP_NAME }}
+ INPUT_QUOTA_FAILED: ${{ inputs.QUOTA_FAILED }}
+ INPUT_TEST_SUCCESS: ${{ inputs.TEST_SUCCESS }}
+ INPUT_TEST_REPORT_URL: ${{ inputs.TEST_REPORT_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 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 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 existing_webapp_url (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
+ else
+ echo "β
existing_webapp_url: '$INPUT_EXISTING_WEBAPP_URL' is valid"
+ fi
+ fi
+
+ # Validate deploy_result (required, must be specific values)
+ if [[ -z "$INPUT_DEPLOY_RESULT" ]]; then
+ echo "β ERROR: deploy_result is required but not provided"
+ VALIDATION_FAILED=true
+ else
+ ALLOWED_DEPLOY_RESULTS=("success" "failure" "skipped")
+ if [[ ! " ${ALLOWED_DEPLOY_RESULTS[@]} " =~ " ${INPUT_DEPLOY_RESULT} " ]]; then
+ echo "β ERROR: deploy_result '$INPUT_DEPLOY_RESULT' is invalid. Allowed values: ${ALLOWED_DEPLOY_RESULTS[*]}"
+ VALIDATION_FAILED=true
+ else
+ echo "β
deploy_result: '$INPUT_DEPLOY_RESULT' is valid"
+ fi
+ fi
+
+ # # Validate e2e_test_result (required, must be specific values)
+ # if [[ -z "$INPUT_E2E_TEST_RESULT" ]]; then
+ # echo "β ERROR: e2e_test_result is required but not provided"
+ # VALIDATION_FAILED=true
+ # else
+ # ALLOWED_TEST_RESULTS=("success" "failure" "skipped")
+ # if [[ ! " ${ALLOWED_TEST_RESULTS[@]} " =~ " ${INPUT_E2E_TEST_RESULT} " ]]; then
+ # echo "β ERROR: e2e_test_result '$INPUT_E2E_TEST_RESULT' is invalid. Allowed values: ${ALLOWED_TEST_RESULTS[*]}"
+ # VALIDATION_FAILED=true
+ # else
+ # echo "β
e2e_test_result: '$INPUT_E2E_TEST_RESULT' is valid"
+ # fi
+ # fi
+
+ # Validate CONTAINER_WEB_APPURL (must start with https if provided)
+ if [[ -n "$INPUT_CONTAINER_WEB_APPURL" ]]; then
+ if [[ ! "$INPUT_CONTAINER_WEB_APPURL" =~ ^https:// ]]; then
+ echo "β ERROR: CONTAINER_WEB_APPURL must start with 'https://', got: '$INPUT_CONTAINER_WEB_APPURL'"
+ VALIDATION_FAILED=true
+ else
+ echo "β
CONTAINER_WEB_APPURL: '$INPUT_CONTAINER_WEB_APPURL' is valid"
+ fi
+ fi
+
+ # Validate RESOURCE_GROUP_NAME (Azure resource group naming convention if provided)
+ 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 QUOTA_FAILED (must be 'true' or 'false' or empty - defaults to 'false')
+ if [[ -n "$INPUT_QUOTA_FAILED" && "$INPUT_QUOTA_FAILED" != "true" && "$INPUT_QUOTA_FAILED" != "false" ]]; then
+ echo "β ERROR: QUOTA_FAILED must be 'true', 'false', or empty, got: '$INPUT_QUOTA_FAILED'"
+ VALIDATION_FAILED=true
+ else
+ echo "β
QUOTA_FAILED: '${INPUT_QUOTA_FAILED:-false}' is valid"
+ fi
+
+ # Validate TEST_SUCCESS (must be 'true' or 'false' or empty)
+ if [[ -n "$INPUT_TEST_SUCCESS" ]]; then
+ if [[ "$INPUT_TEST_SUCCESS" != "true" && "$INPUT_TEST_SUCCESS" != "false" ]]; then
+ echo "β ERROR: TEST_SUCCESS must be 'true', 'false', or empty, got: '$INPUT_TEST_SUCCESS'"
+ VALIDATION_FAILED=true
+ else
+ echo "β
TEST_SUCCESS: '$INPUT_TEST_SUCCESS' is valid"
+ fi
+ fi
+
+ # Validate TEST_REPORT_URL (must start with https if provided)
+ if [[ -n "$INPUT_TEST_REPORT_URL" ]]; then
+ if [[ ! "$INPUT_TEST_REPORT_URL" =~ ^https:// ]]; then
+ echo "β ERROR: TEST_REPORT_URL must start with 'https://', got: '$INPUT_TEST_REPORT_URL'"
+ VALIDATION_FAILED=true
+ else
+ echo "β
TEST_REPORT_URL: '$INPUT_TEST_REPORT_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: Determine Test Suite Display Name
+ # id: test_suite
+ # shell: bash
+ # env:
+ # RUN_E2E_TESTS: ${{ env.RUN_E2E_TESTS }}
+ # run: |
+ # if [ "$RUN_E2E_TESTS" = "GoldenPath-Testing" ]; then
+ # TEST_SUITE_NAME="Golden Path Testing"
+ # elif [ "$RUN_E2E_TESTS" = "Smoke-Testing" ]; then
+ # TEST_SUITE_NAME="Smoke Testing"
+ # elif [ "$RUN_E2E_TESTS" = "None" ]; then
+ # TEST_SUITE_NAME="None"
+ # else
+ # TEST_SUITE_NAME="$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
+ env:
+ GITHUB_REPOSITORY: ${{ github.repository }}
+ GITHUB_RUN_ID: ${{ github.run_id }}
+ ACCELERATOR_NAME: ${{ env.accelerator_name }}
+ LOGICAPP_URL: ${{ secrets.EMAILNOTIFICATION_LOGICAPP_URL_TA }}
+ run: |
+ RUN_URL="https://github.com/${GITHUB_REPOSITORY }/actions/runs/${GITHUB_RUN_ID}"
+ EMAIL_BODY=$(cat <
We would like to inform you that the ${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
We would like to inform you that the ${ACCELERATOR_NAME} deployment process has encountered an issue and has failed to complete successfully.
Deployment Details:
β’ Resource Group: ${RESOURCE_GROUP}
β’ WAF Enabled: ${WAF_ENABLED}
β’ EXP Enabled: ${EXP}
Run URL: ${RUN_URL}
Please investigate the deployment failure at your earliest convenience.
Best regards,
Your Automation Team
We would like to inform you that the ${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: ${WAF_ENABLED}
β’ EXP Enabled: ${EXP}
Run URL: ${RUN_URL}
Best regards,
Your Automation Team
We would like to inform you that the ${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: ${WAF_ENABLED}
β’ EXP Enabled: ${EXP}
Run URL: ${RUN_URL}
Best regards,
Your Automation Team
We would like to inform you that ${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
The ${ACCELERATOR_NAME} pipeline executed against the specified Target 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
The ${ACCELERATOR_NAME} pipeline executed against the specified Target 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