diff --git a/.github/workflows/deploy-linux.yml b/.github/workflows/deploy-ci.yml similarity index 81% rename from .github/workflows/deploy-linux.yml rename to .github/workflows/deploy-ci.yml index c45aae3f5..c8562fe08 100644 --- a/.github/workflows/deploy-linux.yml +++ b/.github/workflows/deploy-ci.yml @@ -1,17 +1,15 @@ -name: Deploy-Test-Cleanup (v2) Linux +name: Deploy-Test-Cleanup (v2) on: pull_request: branches: - main paths: - - 'src/frontend/**' - - 'src/**/*.py' - - 'src/requirements*.txt' - - 'src/WebApp.Dockerfile' - - '!src/tests/**' - - 'infra/**/*.bicep' - - 'infra/**/*.json' - - '*.yaml' + - 'content-gen/src/**' + - '!content-gen/src/tests/**' + - 'content-gen/infra/**/*.bicep' + - 'content-gen/infra/**/*.json' + - 'content-gen/*.yaml' + - 'content-gen/scripts/**' - '.github/workflows/deploy-*.yml' workflow_run: workflows: ["Build Docker and Optional Push"] @@ -23,6 +21,16 @@ on: - demo workflow_dispatch: inputs: + runner_os: + description: 'Deployment Environment' + required: false + type: choice + options: + - 'codespace' + - 'Devcontainer' + - 'Local' + default: 'codespace' + azure_location: description: 'Azure Location For Deployment' required: false @@ -32,11 +40,14 @@ on: - 'australiaeast' - 'centralus' - 'eastasia' - - 'eastus2' + - 'eastus' - 'japaneast' - 'northeurope' - 'southeastasia' + - 'swedencentral' - 'uksouth' + - 'westus' + - 'westus3' resource_group_name: description: 'Resource Group Name (Optional)' required: false @@ -90,17 +101,29 @@ on: required: false default: '' type: string + image_model_choice: + description: 'Image Model to Deploy' + required: false + default: 'gpt-image-1' + type: choice + options: + - 'gpt-image-1' + - 'gpt-image-1.5' + - 'dall-e-3' + - 'none' schedule: - - cron: '0 9,21 * * *' # Runs at 9:00 AM and 9:00 PM GMT + - cron: '30 4 * * *' # Runs at 10:00 AM IST (4:30 AM UTC) permissions: contents: read actions: read + packages: write # Required by deploy-orchestrator → job-deploy → job-deploy-devcontainer for GHCR jobs: validate-inputs: runs-on: ubuntu-latest outputs: validation_passed: ${{ steps.validate.outputs.passed }} + runner_os: ${{ steps.validate.outputs.runner_os }} azure_location: ${{ steps.validate.outputs.azure_location }} resource_group_name: ${{ steps.validate.outputs.resource_group_name }} waf_enabled: ${{ steps.validate.outputs.waf_enabled }} @@ -111,11 +134,13 @@ jobs: 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 }} + image_model_choice: ${{ steps.validate.outputs.image_model_choice }} steps: - name: Validate Workflow Input Parameters id: validate shell: bash env: + INPUT_RUNNER_OS: ${{ github.event.inputs.runner_os }} 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 }} @@ -126,10 +151,30 @@ jobs: 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 }} + INPUT_IMAGE_MODEL_CHOICE: ${{ github.event.inputs.image_model_choice }} run: | echo "🔍 Validating workflow input parameters..." VALIDATION_FAILED=false + # Validate runner_os (specific allowed values) and derive actual runner + RUNNER_OS_INPUT="${INPUT_RUNNER_OS:-codespace}" + if [[ "$RUNNER_OS_INPUT" != "codespace" && "$RUNNER_OS_INPUT" != "Devcontainer" && "$RUNNER_OS_INPUT" != "Local" ]]; then + echo "❌ ERROR: runner_os must be one of: codespace, Devcontainer, Local, got: '$RUNNER_OS_INPUT'" + VALIDATION_FAILED=true + else + echo "✅ runner_os: '$RUNNER_OS_INPUT' is valid" + fi + + # Derive actual runner from runner_os input + if [[ "$RUNNER_OS_INPUT" == "codespace" ]]; then + RUNNER_OS="ubuntu-latest" + elif [[ "$RUNNER_OS_INPUT" == "Devcontainer" ]]; then + RUNNER_OS="devcontainer" + else + RUNNER_OS="windows-latest" + fi + echo "✅ runner_os derived as: '$RUNNER_OS'" + # Validate azure_location (Azure region format) LOCATION="${INPUT_AZURE_LOCATION:-australiaeast}" @@ -252,6 +297,7 @@ jobs: # Output validated values echo "passed=true" >> $GITHUB_OUTPUT + echo "runner_os=$RUNNER_OS" >> $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 @@ -262,13 +308,23 @@ jobs: 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 + + # Validate and output image_model_choice + IMAGE_MODEL="${INPUT_IMAGE_MODEL_CHOICE:-gpt-image-1}" + ALLOWED_MODELS=("gpt-image-1" "gpt-image-1.5" "dall-e-3" "none") + if [[ ! " ${ALLOWED_MODELS[@]} " =~ " ${IMAGE_MODEL} " ]]; then + echo "❌ ERROR: image_model_choice '$IMAGE_MODEL' is invalid. Allowed: ${ALLOWED_MODELS[*]}" + exit 1 + fi + echo "✅ image_model_choice: '$IMAGE_MODEL' is valid" + echo "image_model_choice=$IMAGE_MODEL" >> $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 + runner_os: ${{ needs.validate-inputs.outputs.runner_os || 'ubuntu-latest' }} azure_location: ${{ needs.validate-inputs.outputs.azure_location || 'australiaeast' }} resource_group_name: ${{ needs.validate-inputs.outputs.resource_group_name || '' }} waf_enabled: ${{ needs.validate-inputs.outputs.waf_enabled == 'true' }} @@ -280,4 +336,5 @@ jobs: 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 }} + image_model_choice: ${{ needs.validate-inputs.outputs.image_model_choice || 'gpt-image-1' }} secrets: inherit diff --git a/.github/workflows/deploy-orchestrator.yml b/.github/workflows/deploy-orchestrator.yml index 31741f3b4..a48f50aee 100644 --- a/.github/workflows/deploy-orchestrator.yml +++ b/.github/workflows/deploy-orchestrator.yml @@ -4,7 +4,7 @@ on: workflow_call: inputs: runner_os: - description: 'Runner OS (ubuntu-latest or windows-latest)' + description: 'Runner OS (ubuntu-latest, windows-latest, or devcontainer)' required: true type: string azure_location: @@ -61,12 +61,18 @@ on: description: 'Trigger type (workflow_dispatch, pull_request, schedule)' required: true type: string + image_model_choice: + description: 'Image model to deploy (gpt-image-1, gpt-image-1.5, dall-e-3, none)' + required: false + default: 'gpt-image-1' + type: string env: AZURE_DEV_COLLECT_TELEMETRY: ${{ vars.AZURE_DEV_COLLECT_TELEMETRY }} permissions: contents: read actions: read + packages: write # Required by job-deploy → job-deploy-devcontainer to push devcontainer image to GHCR jobs: docker-build: @@ -94,10 +100,12 @@ jobs: docker_image_tag: ${{ needs.docker-build.outputs.IMAGE_TAG }} run_e2e_tests: ${{ inputs.run_e2e_tests }} cleanup_resources: ${{ inputs.cleanup_resources }} + image_model_choice: ${{ inputs.image_model_choice }} secrets: inherit e2e-test: - if: "!cancelled() && ((needs.deploy.result == 'success' && needs.deploy.outputs.WEB_APPURL != '') || (inputs.existing_webapp_url != '' && inputs.existing_webapp_url != null)) && (inputs.trigger_type != 'workflow_dispatch' || (inputs.run_e2e_tests != 'None' && inputs.run_e2e_tests != '' && inputs.run_e2e_tests != null))" + # if: "!cancelled() && ((needs.deploy.result == 'success' && needs.deploy.outputs.WEB_APPURL != '') || (inputs.existing_webapp_url != '' && inputs.existing_webapp_url != null)) && (inputs.trigger_type != 'workflow_dispatch' || (inputs.run_e2e_tests != 'None' && inputs.run_e2e_tests != '' && inputs.run_e2e_tests != null))" + if: false # Temporarily disable E2E tests needs: [docker-build, deploy] uses: ./.github/workflows/test-automation-v2.yml with: diff --git a/.github/workflows/deploy-windows.yml b/.github/workflows/deploy-windows.yml index 9aec336a2..b2fe3dc3a 100644 --- a/.github/workflows/deploy-windows.yml +++ b/.github/workflows/deploy-windows.yml @@ -86,6 +86,7 @@ on: permissions: contents: read actions: read + packages: write # Required by deploy-orchestrator → job-deploy → job-deploy-devcontainer for GHCR jobs: validate-inputs: diff --git a/.github/workflows/job-deploy-devcontainer.yml b/.github/workflows/job-deploy-devcontainer.yml new file mode 100644 index 000000000..4a864f382 --- /dev/null +++ b/.github/workflows/job-deploy-devcontainer.yml @@ -0,0 +1,436 @@ +name: Deploy Steps - DevContainer + +# The devcontainer (Dockerfile + features + post-create script) installs everything: +# Python 3.11, Azure CLI + Bicep, azd, Node.js, Poetry, pip dependencies +# So the workflow simply: builds the container → logs in → runs azd up. + +on: + workflow_call: + inputs: + ENV_NAME: + required: true + type: string + AZURE_ENV_OPENAI_LOCATION: + required: true + type: string + AZURE_LOCATION: + required: true + type: string + RESOURCE_GROUP_NAME: + required: true + type: string + IMAGE_TAG: + required: true + type: string + BUILD_DOCKER_IMAGE: + required: true + type: string + EXP: + required: true + type: string + WAF_ENABLED: + required: false + type: string + default: 'false' + AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: + required: false + type: string + AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: + required: false + type: string + outputs: + WEB_APPURL: + description: "Container Web App URL" + value: ${{ jobs.deploy-devcontainer.outputs.WEB_APPURL }} + +permissions: + contents: read + packages: write # Required to push devcontainer image to GHCR (optional caching) + +jobs: + deploy-devcontainer: + runs-on: ubuntu-latest + env: + AZURE_DEV_COLLECT_TELEMETRY: ${{ vars.AZURE_DEV_COLLECT_TELEMETRY }} + outputs: + WEB_APPURL: ${{ steps.load_azd_outputs.outputs.WEB_APP_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_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, underscores, and hyphens" + VALIDATION_FAILED=true + else + echo "✅ ENV_NAME: '$INPUT_ENV_NAME' is valid" + fi + + # Validate AZURE_ENV_OPENAI_LOCATION (required, Azure region format) + if [[ -z "$INPUT_AZURE_ENV_OPENAI_LOCATION" ]]; then + echo "❌ ERROR: AZURE_ENV_OPENAI_LOCATION is required but 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, contain only alphanumerics, underscores, periods, hyphens, max 128 characters" + VALIDATION_FAILED=true + else + echo "✅ IMAGE_TAG: '$INPUT_IMAGE_TAG' is valid" + fi + + # Validate EXP (required, boolean string) + 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 (boolean string) + if [[ "$INPUT_WAF_ENABLED" != "true" && "$INPUT_WAF_ENABLED" != "false" ]]; then + echo "❌ ERROR: WAF_ENABLED must be 'true' or 'false', got: '$INPUT_WAF_ENABLED'" + VALIDATION_FAILED=true + else + echo "✅ WAF_ENABLED: '$INPUT_WAF_ENABLED' is valid" + fi + + # Validate AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID (optional, Azure Resource ID format) + if [[ -n "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID" ]]; then + if [[ ! "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/[Mm]icrosoft\.[Oo]perational[Ii]nsights/[Ww]orkspaces/[^/]+$ ]]; then + echo "❌ ERROR: AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID is invalid. Must be a valid Azure Resource ID format:" + echo " /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.OperationalInsights/workspaces/{workspaceName}" + echo " Got: '$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID'" + VALIDATION_FAILED=true + else + echo "✅ AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: Valid Resource ID format" + fi + else + echo "✅ AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: Not provided (optional)" + 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-]+/[Rr]esource[Gg]roups/[^/]+/providers/([Mm]icrosoft\.[Mm]achine[Ll]earning[Ss]ervices/([Ww]orkspaces|[Pp]rojects)/[^/]+|[Mm]icrosoft\.[Cc]ognitive[Ss]ervices/[Aa]ccounts/[^/]+/[Pp]rojects/[^/]+)$ ]]; 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 + + # 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: + WAF_ENABLED: ${{ inputs.WAF_ENABLED }} + run: | + if [[ "$WAF_ENABLED" == "true" ]]; then + cp content-gen/infra/main.waf.parameters.json content-gen/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: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set Lowercase Image Name + id: image_name + shell: bash + run: echo "IMAGE_NAME=ghcr.io/$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')/devcontainer" >> $GITHUB_ENV + + # Build devcontainer and run azd up inside it + - name: Deploy with azd up in DevContainer + uses: devcontainers/ci@v0.3 + env: + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + 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 }} + SECRET_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ secrets.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }} + SECRET_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ secrets.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }} + SECRET_ACR_TEST_USERNAME: ${{ secrets.ACR_TEST_USERNAME }} + with: + # Cache the built devcontainer image to GHCR to reuse across runs + imageName: ${{ env.IMAGE_NAME }} + cacheFrom: ${{ env.IMAGE_NAME }} + push: always + + # Explicitly forward env vars into the devcontainer + env: | + AZURE_CLIENT_ID + AZURE_CLIENT_SECRET + AZURE_TENANT_ID + AZURE_SUBSCRIPTION_ID + INPUT_ENV_NAME + INPUT_AZURE_ENV_OPENAI_LOCATION + INPUT_AZURE_LOCATION + INPUT_RESOURCE_GROUP_NAME + INPUT_IMAGE_TAG + INPUT_BUILD_DOCKER_IMAGE + INPUT_EXP + INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID + INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID + SECRET_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID + SECRET_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID + SECRET_ACR_TEST_USERNAME + + runCmd: | + echo "đŸŗ Running inside devcontainer — same environment as Codespaces" + echo "Python: $(python3 --version)" + echo "Azure CLI: $(az version --query '"azure-cli"' -o tsv)" + echo "azd: $(azd version)" + echo "" + + set -e + + # 1. Authenticate Azure CLI and azd using client secret + echo "🔐 Logging in with Azure CLI (client secret)..." + az login --service-principal -u "$AZURE_CLIENT_ID" -p "$AZURE_CLIENT_SECRET" --tenant "$AZURE_TENANT_ID" + az account set --subscription "$AZURE_SUBSCRIPTION_ID" + + echo "🔐 Logging in with azd (client secret)..." + azd auth login \ + --client-id "$AZURE_CLIENT_ID" \ + --client-secret "$AZURE_CLIENT_SECRET" \ + --tenant-id "$AZURE_TENANT_ID" + + # 2. Change to content-gen directory where azure.yaml lives + cd content-gen + + # Initialize azd environment + echo "âš™ī¸ Creating azd environment: $INPUT_ENV_NAME" + azd env new "$INPUT_ENV_NAME" --no-prompt + + echo "Setting default subscription..." + azd config set defaults.subscription "$AZURE_SUBSCRIPTION_ID" + + # Set core parameters + azd env set AZURE_SUBSCRIPTION_ID="$AZURE_SUBSCRIPTION_ID" + azd env set AZURE_ENV_OPENAI_LOCATION="$INPUT_AZURE_ENV_OPENAI_LOCATION" + azd env set AZURE_LOCATION="$INPUT_AZURE_LOCATION" + azd env set AZURE_RESOURCE_GROUP="$INPUT_RESOURCE_GROUP_NAME" + + # Set ACR endpoint based on BUILD_DOCKER_IMAGE flag + if [[ "$INPUT_BUILD_DOCKER_IMAGE" == "true" ]]; then + ACR_NAME=$(echo "$SECRET_ACR_TEST_USERNAME") + azd env set ACR_NAME="$ACR_NAME" + echo "Set ACR name to: $ACR_NAME" + else + echo "Skipping ACR name configuration (using existing image)" + fi + + # Set EXP parameters if enabled + 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="$SECRET_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="$SECRET_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" + if [[ -n "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID" ]] || [[ -n "$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" ]]; then + echo "âš ī¸ Warning: EXP parameter values provided but EXP is disabled. These values will be ignored." + fi + fi + + # 3. Deploy using azd up + echo "🚀 Running azd up..." + azd up --no-prompt + + # 4. Export azd env values to a file so the host can read them + echo "📝 Exporting azd environment values..." + azd env get-values > /tmp/azd-env-values.txt 2>/dev/null || true + cp /tmp/azd-env-values.txt "$PWD/.azd-env-output" || true + + # Also copy to the workspace root for the host step + cp /tmp/azd-env-values.txt "$GITHUB_WORKSPACE/.azd-env-output" || true + + echo "" + echo "✅ Deployment complete!" + + - name: Load azd Environment Outputs + id: load_azd_outputs + if: always() + shell: bash + run: | + # Read the azd env values exported from the devcontainer + AZD_OUTPUT_FILE="" + if [ -f .azd-env-output ]; then + AZD_OUTPUT_FILE=".azd-env-output" + elif [ -f content-gen/.azd-env-output ]; then + AZD_OUTPUT_FILE="content-gen/.azd-env-output" + fi + + if [ -n "$AZD_OUTPUT_FILE" ]; then + echo "📝 Loading azd environment values from $AZD_OUTPUT_FILE..." + + # Parse azd env output (format: KEY="value") + while IFS= read -r line; do + key=$(echo "$line" | cut -d'=' -f1) + value=$(echo "$line" | cut -d'=' -f2- | sed 's/^"//' | sed 's/"$//') + if [ -n "$key" ] && [ -n "$value" ]; then + echo "${key}=${value}" >> $GITHUB_ENV + fi + done < "$AZD_OUTPUT_FILE" + + echo "✅ Loaded azd environment values" + + # Extract Web App URL for downstream jobs + WEB_APP_URL=$(grep '^WEB_APP_URL=' "$AZD_OUTPUT_FILE" | cut -d'=' -f2- | sed 's/^"//' | sed 's/"$//') + echo "WEB_APP_URL=$WEB_APP_URL" >> $GITHUB_OUTPUT + else + echo "âš ī¸ No azd env output file found (.azd-env-output)" + fi + + - name: Generate Deploy Job Summary + if: always() + env: + RESOURCE_GROUP_NAME: ${{ inputs.RESOURCE_GROUP_NAME }} + WAF_ENABLED: ${{ inputs.WAF_ENABLED }} + EXP: ${{ inputs.EXP }} + AZURE_LOCATION: ${{ inputs.AZURE_LOCATION }} + AZURE_ENV_OPENAI_LOCATION: ${{ inputs.AZURE_ENV_OPENAI_LOCATION }} + IMAGE_TAG: ${{ inputs.IMAGE_TAG }} + JOB_STATUS: ${{ job.status }} + WEB_APP_URL: ${{ steps.load_azd_outputs.outputs.WEB_APP_URL }} + run: | + echo "## đŸŗ Deploy Job Summary (DevContainer)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY + + if [[ "$JOB_STATUS" == "success" ]]; then + echo "| **Job Status** | ✅ Success |" >> $GITHUB_STEP_SUMMARY + else + echo "| **Job Status** | ❌ Failed |" >> $GITHUB_STEP_SUMMARY + fi + + echo "| **Resource Group** | \`$RESOURCE_GROUP_NAME\` |" >> $GITHUB_STEP_SUMMARY + + # Determine configuration type + if [[ "$WAF_ENABLED" == "true" && "$EXP" == "true" ]]; then + CONFIG_TYPE="WAF + EXP" + elif [[ "$WAF_ENABLED" == "true" && "$EXP" != "true" ]]; then + CONFIG_TYPE="WAF + Non-EXP" + elif [[ "$WAF_ENABLED" != "true" && "$EXP" == "true" ]]; then + CONFIG_TYPE="Non-WAF + EXP" + else + CONFIG_TYPE="Non-WAF + Non-EXP" + fi + echo "| **Configuration Type** | \`$CONFIG_TYPE\` |" >> $GITHUB_STEP_SUMMARY + + echo "| **Azure Region (Infrastructure)** | \`$AZURE_LOCATION\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Azure OpenAI Region** | \`$AZURE_ENV_OPENAI_LOCATION\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Docker Image Tag** | \`$IMAGE_TAG\` |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [[ "$JOB_STATUS" == "success" ]]; then + echo "### ✅ Deployment Details" >> $GITHUB_STEP_SUMMARY + echo "- **Web App URL**: [$WEB_APP_URL]($WEB_APP_URL)" >> $GITHUB_STEP_SUMMARY + echo "- Successfully deployed to Azure using devcontainer 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 deploy job for detailed error information" >> $GITHUB_STEP_SUMMARY + fi + + - name: Logout from Azure + if: always() + shell: bash + run: | + az logout || true + echo "Logged out from Azure." diff --git a/.github/workflows/job-deploy-linux.yml b/.github/workflows/job-deploy-linux.yml index 37d1b82a2..e4338572a 100644 --- a/.github/workflows/job-deploy-linux.yml +++ b/.github/workflows/job-deploy-linux.yml @@ -37,7 +37,7 @@ on: outputs: WEB_APPURL: description: "Container Web App URL" - value: ${{ jobs.deploy-linux.outputs.WEB_APPURL }} + value: ${{ jobs.deploy-linux.outputs.WEB_APP_URL }} permissions: contents: read actions: read @@ -48,7 +48,7 @@ jobs: env: AZURE_DEV_COLLECT_TELEMETRY: ${{ vars.AZURE_DEV_COLLECT_TELEMETRY }} outputs: - WEB_APPURL: ${{ steps.get_output_linux.outputs.WEB_APPURL }} + WEB_APP_URL: ${{ steps.get_output_linux.outputs.WEB_APP_URL }} steps: - name: Validate Workflow Input Parameters shell: bash @@ -191,7 +191,7 @@ jobs: WAF_ENABLED: ${{ inputs.WAF_ENABLED }} run: | if [[ "$WAF_ENABLED" == "true" ]]; then - cp infra/main.waf.parameters.json infra/main.parameters.json + cp content-gen/infra/main.waf.parameters.json content-gen/infra/main.parameters.json echo "✅ Successfully copied WAF parameters to main parameters file" else echo "🔧 Configuring Non-WAF deployment - using default main.parameters.json..." @@ -224,6 +224,9 @@ jobs: run: | set -e + # Change to content-gen directory where azure.yaml lives + cd content-gen + echo "Creating environment..." azd env new "$ENV_NAME" --no-prompt echo "Environment created: $ENV_NAME" @@ -236,13 +239,13 @@ jobs: azd env set AZURE_ENV_OPENAI_LOCATION="$AZURE_ENV_OPENAI_LOCATION" azd env set AZURE_LOCATION="$AZURE_LOCATION" azd env set AZURE_RESOURCE_GROUP="$RESOURCE_GROUP_NAME" - azd env set AZURE_ENV_IMAGETAG="$IMAGE_TAG" + azd env set IMAGE_TAG="$IMAGE_TAG" # Set ACR name only when building Docker image if [[ "$BUILD_DOCKER_IMAGE" == "true" ]]; then # Extract ACR name from login server and set as environment variable ACR_NAME="${{ secrets.ACR_TEST_USERNAME }}" - azd env set AZURE_ENV_ACR_NAME="$ACR_NAME" + azd env set ACR_NAME="$ACR_NAME" echo "Set ACR name to: $ACR_NAME" else echo "Skipping ACR name configuration (using existing image)" @@ -279,68 +282,9 @@ jobs: azd up --no-prompt # Get deployment outputs using azd - echo "Extracting deployment outputs..." - DEPLOY_OUTPUT=$(azd env get-values --output json) - echo "Deployment output: $DEPLOY_OUTPUT" - - if [[ -z "$DEPLOY_OUTPUT" ]]; then - echo "Error: Deployment output is empty. Please check the deployment logs." - exit 1 - fi - - # Extract values from azd output (adjust these based on actual output variable names) - AI_FOUNDRY_RESOURCE_ID=$(echo "$DEPLOY_OUTPUT" | jq -r '.AI_FOUNDRY_RESOURCE_ID // empty') - echo "AI_FOUNDRY_RESOURCE_ID=$AI_FOUNDRY_RESOURCE_ID" >> $GITHUB_ENV - - AI_SEARCH_SERVICE_NAME=$(echo "$DEPLOY_OUTPUT" | jq -r '.AI_SEARCH_SERVICE_NAME // empty') - echo "AI_SEARCH_SERVICE_NAME=$AI_SEARCH_SERVICE_NAME" >> $GITHUB_ENV - - AZURE_COSMOSDB_ACCOUNT=$(echo "$DEPLOY_OUTPUT" | jq -r '.AZURE_COSMOSDB_ACCOUNT // empty') - echo "AZURE_COSMOSDB_ACCOUNT=$AZURE_COSMOSDB_ACCOUNT" >> $GITHUB_ENV - - STORAGE_ACCOUNT_NAME=$(echo "$DEPLOY_OUTPUT" | jq -r '.STORAGE_ACCOUNT_NAME // empty') - echo "STORAGE_ACCOUNT_NAME=$STORAGE_ACCOUNT_NAME" >> $GITHUB_ENV - - STORAGE_CONTAINER_NAME=$(echo "$DEPLOY_OUTPUT" | jq -r '.STORAGE_CONTAINER_NAME // empty') - echo "STORAGE_CONTAINER_NAME=$STORAGE_CONTAINER_NAME" >> $GITHUB_ENV - - KEY_VAULT_NAME=$(echo "$DEPLOY_OUTPUT" | jq -r '.KEY_VAULT_NAME // empty') - echo "KEY_VAULT_NAME=$KEY_VAULT_NAME" >> $GITHUB_ENV - - RESOURCE_GROUP_NAME=$(echo "$DEPLOY_OUTPUT" | jq -r '.RESOURCE_GROUP_NAME // empty') - echo "RESOURCE_GROUP_NAME=$RESOURCE_GROUP_NAME" >> $GITHUB_ENV - - WEB_APPURL=$(echo "$DEPLOY_OUTPUT" | jq -r '.WEB_APP_URL // .SERVICE_BACKEND_ENDPOINT_URL // empty') - echo "WEB_APPURL=$WEB_APPURL" >> $GITHUB_OUTPUT - sleep 30 - - - name: Run Post-Deployment Script - id: post_deploy - env: - AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} - STORAGE_ACCOUNT_NAME: ${{ env.STORAGE_ACCOUNT_NAME }} - STORAGE_CONTAINER_NAME: ${{ env.STORAGE_CONTAINER_NAME }} - KEY_VAULT_NAME: ${{ env.KEY_VAULT_NAME }} - AZURE_COSMOSDB_ACCOUNT: ${{ env.AZURE_COSMOSDB_ACCOUNT }} - RESOURCE_GROUP_NAME: ${{ env.RESOURCE_GROUP_NAME }} - AI_SEARCH_SERVICE_NAME: ${{ env.AI_SEARCH_SERVICE_NAME }} - AI_FOUNDRY_RESOURCE_ID: ${{ env.AI_FOUNDRY_RESOURCE_ID }} - run: | - set -e - az account set --subscription "$AZURE_SUBSCRIPTION_ID" - - echo "Running post-deployment script..." - - bash ./infra/scripts/process_sample_data.sh \ - "$STORAGE_ACCOUNT_NAME" \ - "$STORAGE_CONTAINER_NAME" \ - "$KEY_VAULT_NAME" \ - "$AZURE_COSMOSDB_ACCOUNT" \ - "$RESOURCE_GROUP_NAME" \ - "$AI_SEARCH_SERVICE_NAME" \ - "$AZURE_CLIENT_ID" \ - "$AI_FOUNDRY_RESOURCE_ID" + WEB_APP_URL=$(azd env get-value WEB_APP_URL) + echo "WEB_APP_URL=$WEB_APP_URL" >> $GITHUB_ENV + echo "WEB_APP_URL=$WEB_APP_URL" >> $GITHUB_OUTPUT - name: Generate Deploy Job Summary if: always() @@ -352,7 +296,7 @@ jobs: AZURE_ENV_OPENAI_LOCATION: ${{ inputs.AZURE_ENV_OPENAI_LOCATION }} IMAGE_TAG: ${{ inputs.IMAGE_TAG }} JOB_STATUS: ${{ job.status }} - WEB_APPURL: ${{ steps.get_output_linux.outputs.WEB_APPURL }} + WEB_APP_URL: ${{ steps.get_output_linux.outputs.WEB_APP_URL }} run: | echo "## 🚀 Deploy Job Summary (Linux)" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY @@ -386,9 +330,9 @@ jobs: if [[ "$JOB_STATUS" == "success" ]]; then echo "### ✅ Deployment Details" >> $GITHUB_STEP_SUMMARY - echo "- **Web App URL**: [$WEB_APPURL]($WEB_APPURL)" >> $GITHUB_STEP_SUMMARY + echo "- **Web App URL**: [$WEB_APP_URL]($WEB_APP_URL)" >> $GITHUB_STEP_SUMMARY echo "- Successfully deployed to Azure with all resources configured" >> $GITHUB_STEP_SUMMARY - echo "- Post-deployment scripts executed successfully" >> $GITHUB_STEP_SUMMARY + echo "- All Deployment scripts executed successfully" >> $GITHUB_STEP_SUMMARY else echo "### ❌ Deployment Failed" >> $GITHUB_STEP_SUMMARY echo "- Deployment process encountered an error" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/job-deploy-windows.yml b/.github/workflows/job-deploy-windows.yml index e9dda12d4..aba12f4d1 100644 --- a/.github/workflows/job-deploy-windows.yml +++ b/.github/workflows/job-deploy-windows.yml @@ -47,6 +47,7 @@ jobs: runs-on: windows-latest env: AZURE_DEV_COLLECT_TELEMETRY: ${{ vars.AZURE_DEV_COLLECT_TELEMETRY }} + PYTHONUTF8: 1 outputs: WEB_APPURL: ${{ steps.get_output_windows.outputs.WEB_APPURL }} steps: @@ -192,7 +193,7 @@ jobs: WAF_ENABLED: ${{ inputs.WAF_ENABLED }} run: | if [[ "$WAF_ENABLED" == "true" ]]; then - cp infra/main.waf.parameters.json infra/main.parameters.json + cp content-gen/infra/main.waf.parameters.json content-gen/infra/main.parameters.json echo "✅ Successfully copied WAF parameters to main parameters file" else echo "🔧 Configuring Non-WAF deployment - using default main.parameters.json..." @@ -226,6 +227,9 @@ jobs: run: | $ErrorActionPreference = "Stop" + # Change to content-gen directory where azure.yaml lives + Push-Location content-gen + Write-Host "Creating environment..." azd env new $env:ENV_NAME --no-prompt Write-Host "Environment created: $env:ENV_NAME" @@ -238,12 +242,12 @@ jobs: azd env set AZURE_ENV_OPENAI_LOCATION="$env:AZURE_ENV_OPENAI_LOCATION" azd env set AZURE_LOCATION="$env:AZURE_LOCATION" azd env set AZURE_RESOURCE_GROUP="$env:RESOURCE_GROUP_NAME" - azd env set AZURE_ENV_IMAGETAG="$env:IMAGE_TAG" + azd env set IMAGE_TAG="$env:IMAGE_TAG" # Set ACR name only when building Docker image if ($env:BUILD_DOCKER_IMAGE -eq "true") { $ACR_NAME = "${{ secrets.ACR_TEST_USERNAME }}" - azd env set AZURE_ENV_ACR_NAME="$ACR_NAME" + azd env set ACR_NAME="$ACR_NAME" Write-Host "Set ACR name to: $ACR_NAME" } else { Write-Host "Skipping ACR name configuration (using existing image)" @@ -275,65 +279,14 @@ jobs: # Deploy using azd up azd up --no-prompt - + Write-Host "✅ Deployment succeeded." # Get deployment outputs using azd Write-Host "Extracting deployment outputs..." - $DEPLOY_OUTPUT = azd env get-values --output json | ConvertFrom-Json - Write-Host "Deployment output: $($DEPLOY_OUTPUT | ConvertTo-Json -Depth 10)" - - if (-not $DEPLOY_OUTPUT) { - Write-Host "Error: Deployment output is empty. Please check the deployment logs." - exit 1 - } - - - $AI_FOUNDRY_RESOURCE_ID = $DEPLOY_OUTPUT.AI_FOUNDRY_RESOURCE_ID - "AI_FOUNDRY_RESOURCE_ID=$AI_FOUNDRY_RESOURCE_ID" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - - $AI_SEARCH_SERVICE_NAME = $DEPLOY_OUTPUT.AI_SEARCH_SERVICE_NAME - "AI_SEARCH_SERVICE_NAME=$AI_SEARCH_SERVICE_NAME" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - - $AZURE_COSMOSDB_ACCOUNT = $DEPLOY_OUTPUT.AZURE_COSMOSDB_ACCOUNT - "AZURE_COSMOSDB_ACCOUNT=$AZURE_COSMOSDB_ACCOUNT" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - - $STORAGE_ACCOUNT_NAME = $DEPLOY_OUTPUT.STORAGE_ACCOUNT_NAME - "STORAGE_ACCOUNT_NAME=$STORAGE_ACCOUNT_NAME" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - - $STORAGE_CONTAINER_NAME = $DEPLOY_OUTPUT.STORAGE_CONTAINER_NAME - "STORAGE_CONTAINER_NAME=$STORAGE_CONTAINER_NAME" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - - $KEY_VAULT_NAME = $DEPLOY_OUTPUT.KEY_VAULT_NAME - "KEY_VAULT_NAME=$KEY_VAULT_NAME" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - - $RESOURCE_GROUP_NAME = $DEPLOY_OUTPUT.RESOURCE_GROUP_NAME - "RESOURCE_GROUP_NAME=$RESOURCE_GROUP_NAME" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - - $WEB_APP_URL = $DEPLOY_OUTPUT.WEB_APP_URL - "WEB_APPURL=$WEB_APP_URL" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - "WEB_APPURL=$WEB_APP_URL" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append - - - name: Run Post-Deployment Script - id: post_deploy - env: - AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - shell: bash - run: | - set -e - az account set --subscription "${{ secrets.AZURE_SUBSCRIPTION_ID }}" - - echo "Running post-deployment script..." - - bash ./infra/scripts/process_sample_data.sh \ - "${{ env.STORAGE_ACCOUNT_NAME }}" \ - "${{ env.STORAGE_CONTAINER_NAME }}" \ - "${{ env.KEY_VAULT_NAME }}" \ - "${{ env.AZURE_COSMOSDB_ACCOUNT }}" \ - "${{ env.RESOURCE_GROUP_NAME }}" \ - "${{ env.AI_SEARCH_SERVICE_NAME }}" \ - "${{ secrets.AZURE_CLIENT_ID }}" \ - "${{ env.AI_FOUNDRY_RESOURCE_ID }}" + $webAppUrl = azd env get-value WEB_APP_URL + echo "WEB_APPURL=$webAppUrl" >> $env:GITHUB_ENV + echo "WEB_APPURL=$webAppUrl" >> $env:GITHUB_OUTPUT - name: Generate Deploy Job Summary if: always() diff --git a/.github/workflows/job-deploy.yml b/.github/workflows/job-deploy.yml index a54023768..37dfa8395 100644 --- a/.github/workflows/job-deploy.yml +++ b/.github/workflows/job-deploy.yml @@ -66,13 +66,18 @@ on: required: false default: '' type: string + image_model_choice: + description: 'Image model to deploy (gpt-image-1, gpt-image-1.5, dall-e-3, none)' + required: false + default: 'gpt-image-1' + type: string outputs: RESOURCE_GROUP_NAME: description: "Resource Group Name" value: ${{ jobs.azure-setup.outputs.RESOURCE_GROUP_NAME }} WEB_APPURL: description: "Container Web App URL" - value: ${{ jobs.deploy-linux.outputs.WEB_APPURL || jobs.deploy-windows.outputs.WEB_APPURL }} + value: ${{ jobs.deploy-linux.outputs.WEB_APPURL || jobs.deploy-windows.outputs.WEB_APPURL || jobs.deploy-devcontainer.outputs.WEB_APPURL }} ENV_NAME: description: "Environment Name" value: ${{ jobs.azure-setup.outputs.ENV_NAME }} @@ -91,7 +96,7 @@ on: env: GPT_MIN_CAPACITY: 150 - TEXT_EMBEDDING_MIN_CAPACITY: 80 + IMAGE_MODEL_MIN_CAPACITY: 1 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 }} @@ -101,6 +106,7 @@ env: permissions: contents: read actions: read + packages: write # Required by job-deploy-devcontainer to push devcontainer image to GHCR jobs: azure-setup: @@ -149,7 +155,7 @@ jobs: fi # Validate runner_os (required - must be specific values) - ALLOWED_RUNNER_OS=("ubuntu-latest" "windows-latest") + ALLOWED_RUNNER_OS=("ubuntu-latest" "windows-latest" "devcontainer") if [[ -z "$INPUT_RUNNER_OS" ]]; then echo "❌ ERROR: runner_os is required but was not provided" VALIDATION_FAILED=true @@ -333,13 +339,14 @@ jobs: AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} GPT_MIN_CAPACITY: ${{ env.GPT_MIN_CAPACITY }} - TEXT_EMBEDDING_MIN_CAPACITY: ${{ env.TEXT_EMBEDDING_MIN_CAPACITY }} + IMAGE_MODEL_CHOICE: ${{ inputs.image_model_choice || 'gpt-image-1' }} + IMAGE_MODEL_MIN_CAPACITY: ${{ env.IMAGE_MODEL_MIN_CAPACITY }} AZURE_REGIONS: ${{ vars.AZURE_REGIONS }} run: | - chmod +x scripts/checkquota.sh - if ! scripts/checkquota.sh; then + chmod +x content-gen/scripts/checkquota.sh + if ! content-gen/scripts/checkquota.sh; then # If quota check fails due to insufficient quota, set the flag - if grep -q "No region with sufficient quota found" scripts/checkquota.sh; then + if grep -q "No region with sufficient quota found" content-gen/scripts/checkquota.sh; then echo "QUOTA_FAILED=true" >> $GITHUB_ENV fi exit 1 # Fail the pipeline if any other failure occurs @@ -358,25 +365,142 @@ jobs: shell: bash run: exit 1 + - name: Check Azure Search Service Quota + id: search-quota-check + shell: bash + env: + INPUT_AZURE_LOCATION: ${{ inputs.azure_location }} + TRIGGER_TYPE: ${{ inputs.trigger_type }} + run: | + # Determine search tier based on WAF (scalability) setting + # WAF enabled → standard tier, WAF disabled → basic tier + if [[ "${{ env.WAF_ENABLED }}" == "true" ]]; then + SEARCH_TIER="standard" + SEARCH_TIER_DISPLAY="Standard" + else + SEARCH_TIER="basic" + SEARCH_TIER_DISPLAY="Basic" + fi + + # Valid deployment regions (must match Bicep @allowed list) + ALL_VALID_REGIONS=("australiaeast" "centralus" "eastasia" "eastus" "japaneast" "northeurope" "southeastasia" "swedencentral" "uksouth" "westus" "westus3") + + # Build ordered list of regions to check: + # - Manual triggers: user's region first, then remaining valid regions + # - Automatic triggers: all valid regions (no user preference) + REGIONS_TO_CHECK=() + if [[ "$TRIGGER_TYPE" == "workflow_dispatch" && -n "$INPUT_AZURE_LOCATION" ]]; then + echo "📋 Manual trigger: checking user-selected region '$INPUT_AZURE_LOCATION' first" + REGIONS_TO_CHECK+=("$INPUT_AZURE_LOCATION") + for r in "${ALL_VALID_REGIONS[@]}"; do + if [[ "$r" != "$INPUT_AZURE_LOCATION" ]]; then + REGIONS_TO_CHECK+=("$r") + fi + done + else + echo "📋 Automatic trigger: checking all valid regions for search quota" + REGIONS_TO_CHECK=("${ALL_VALID_REGIONS[@]}") + fi + + echo "🔍 Checking Azure Search $SEARCH_TIER_DISPLAY tier quota across regions: ${REGIONS_TO_CHECK[*]}" + echo "" + + SEARCH_VALID_REGION="" + CHECKED_REGIONS=() + for REGION in "${REGIONS_TO_CHECK[@]}"; do + echo "----------------------------------------" + echo "🔍 Checking region: $REGION" + + SEARCH_USAGE=$(az rest --method get \ + --url "https://management.azure.com/subscriptions/${{ secrets.AZURE_SUBSCRIPTION_ID }}/providers/Microsoft.Search/locations/${REGION}/usages?api-version=2024-03-01-preview" \ + -o json 2>/dev/null || echo '{"value":[]}') + + if [ "$(echo "$SEARCH_USAGE" | jq '.value | length')" -eq 0 ]; then + echo " âš ī¸ Could not retrieve Azure Search quota for $REGION. Skipping." + CHECKED_REGIONS+=("$REGION: skipped") + continue + fi + + TIER_USAGE=$(echo "$SEARCH_USAGE" | jq -r --arg tier "$SEARCH_TIER" '.value[] | select(.name.value == $tier) | .currentValue // 0') + TIER_LIMIT=$(echo "$SEARCH_USAGE" | jq -r --arg tier "$SEARCH_TIER" '.value[] | select(.name.value == $tier) | .limit // 0') + TIER_AVAILABLE=$((TIER_LIMIT - TIER_USAGE)) + + echo " $SEARCH_TIER_DISPLAY Tier: Used=$TIER_USAGE | Limit=$TIER_LIMIT | Available=$TIER_AVAILABLE" + + if [ "$TIER_AVAILABLE" -ge 1 ]; then + echo " ✅ Region '$REGION' has sufficient Azure Search $SEARCH_TIER_DISPLAY quota!" + SEARCH_VALID_REGION="$REGION" + CHECKED_REGIONS+=("$REGION: ✅ available=$TIER_AVAILABLE") + break + else + echo " ❌ Insufficient quota in $REGION" + CHECKED_REGIONS+=("$REGION: ❌ available=$TIER_AVAILABLE") + fi + done + + echo "" + echo "========================================" + if [ -z "$SEARCH_VALID_REGION" ]; then + echo "❌ No region with sufficient Azure Search $SEARCH_TIER_DISPLAY tier quota found!" + echo "" + echo "Regions checked:" + for entry in "${CHECKED_REGIONS[@]}"; do + echo " - $entry" + done + + # Add error to GitHub Summary + echo "## ❌ Azure Search Quota Check Failed" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "No region with sufficient Azure Search **$SEARCH_TIER_DISPLAY** tier quota was found." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Region | Status |" >> $GITHUB_STEP_SUMMARY + echo "|--------|--------|" >> $GITHUB_STEP_SUMMARY + for entry in "${CHECKED_REGIONS[@]}"; do + IFS=': ' read -r region status <<< "$entry" + echo "| \`$region\` | $status |" >> $GITHUB_STEP_SUMMARY + done + exit 1 + fi + + echo "✅ Selected region with Azure Search quota: $SEARCH_VALID_REGION" + echo "SEARCH_VALID_REGION=$SEARCH_VALID_REGION" >> $GITHUB_ENV + echo "SEARCH_VALID_REGION=$SEARCH_VALID_REGION" >> $GITHUB_OUTPUT + echo "SEARCH_QUOTA_CHECK=passed" >> $GITHUB_OUTPUT + + if [[ "$TRIGGER_TYPE" == "workflow_dispatch" && "$SEARCH_VALID_REGION" != "$INPUT_AZURE_LOCATION" ]]; then + echo "" + echo "âš ī¸ User-selected region '$INPUT_AZURE_LOCATION' had insufficient search quota." + echo " Falling back to region: $SEARCH_VALID_REGION" + fi + - name: Set Deployment Region id: set_region shell: bash env: INPUT_AZURE_LOCATION: ${{ inputs.azure_location }} run: | - echo "Selected Region from Quota Check: $VALID_REGION" + echo "Selected Region from OpenAI Quota Check: $VALID_REGION" + echo "Selected Region from Search Quota Check: $SEARCH_VALID_REGION" echo "AZURE_ENV_OPENAI_LOCATION=$VALID_REGION" >> $GITHUB_ENV echo "AZURE_ENV_OPENAI_LOCATION=$VALID_REGION" >> $GITHUB_OUTPUT - + if [[ "${{ inputs.trigger_type }}" == "workflow_dispatch" && -n "$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 + # Manual trigger: use user's region if it passed search quota, otherwise use search-validated region + if [[ "$INPUT_AZURE_LOCATION" == "$SEARCH_VALID_REGION" ]]; then + echo "✅ Using user-selected Azure location (search quota verified): $INPUT_AZURE_LOCATION" + echo "AZURE_LOCATION=$INPUT_AZURE_LOCATION" >> $GITHUB_ENV + echo "AZURE_LOCATION=$INPUT_AZURE_LOCATION" >> $GITHUB_OUTPUT + else + echo "âš ī¸ User-selected region '$INPUT_AZURE_LOCATION' had insufficient search quota." + echo " Using fallback region from search quota check: $SEARCH_VALID_REGION" + echo "AZURE_LOCATION=$SEARCH_VALID_REGION" >> $GITHUB_ENV + echo "AZURE_LOCATION=$SEARCH_VALID_REGION" >> $GITHUB_OUTPUT + fi 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 + # Automatic trigger: use the search-quota-validated region + echo "Using region from search quota check for automatic triggers: $SEARCH_VALID_REGION" + echo "AZURE_LOCATION=$SEARCH_VALID_REGION" >> $GITHUB_ENV + echo "AZURE_LOCATION=$SEARCH_VALID_REGION" >> $GITHUB_OUTPUT fi - name: Generate Resource Group Name @@ -391,7 +515,7 @@ jobs: echo "RESOURCE_GROUP_NAME=$INPUT_RESOURCE_GROUP_NAME" >> $GITHUB_ENV else echo "Generating a unique resource group name..." - ACCL_NAME="docgen" # Account name as specified + ACCL_NAME="cntgen" # 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 @@ -451,8 +575,8 @@ jobs: # Determine image tag based on branch if [[ "$BRANCH_NAME" == "main" ]]; then - IMAGE_TAG="latest_waf" - echo "Using main branch - image tag: latest_waf" + IMAGE_TAG="latest" + echo "Using main branch - image tag: latest" elif [[ "$BRANCH_NAME" == "dev" ]]; then IMAGE_TAG="dev" echo "Using dev branch - image tag: dev" @@ -460,8 +584,8 @@ jobs: IMAGE_TAG="demo" echo "Using demo branch - image tag: demo" else - IMAGE_TAG="latest_waf" - echo "Using default for branch '$BRANCH_NAME' - image tag: latest_waf" + IMAGE_TAG="latest" + echo "Using default for branch '$BRANCH_NAME' - image tag: latest" fi echo "Using existing Docker image tag: $IMAGE_TAG" @@ -552,3 +676,21 @@ jobs: 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-devcontainer: + name: Deploy on DevContainer + needs: azure-setup + if: inputs.runner_os == 'devcontainer' && !cancelled() && needs.azure-setup.result == 'success' + uses: ./.github/workflows/job-deploy-devcontainer.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: ${{ needs.azure-setup.outputs.EXP_ENABLED }} + 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 index fc564ea3f..41e581966 100644 --- a/.github/workflows/job-docker-build.yml +++ b/.github/workflows/job-docker-build.yml @@ -63,8 +63,8 @@ jobs: env: DOCKER_BUILD_SUMMARY: false with: - context: ./src - file: ./src/WebApp.Dockerfile + context: ./content-gen/src/app + file: ./content-gen/src/app/WebApp.Dockerfile push: true tags: | ${{ secrets.ACR_TEST_LOGIN_SERVER }}/webapp:${{ steps.generate_docker_tag.outputs.IMAGE_TAG }} diff --git a/content-gen/docs/AZD_DEPLOYMENT.md b/content-gen/docs/AZD_DEPLOYMENT.md index 0348c9de6..693b888ba 100644 --- a/content-gen/docs/AZD_DEPLOYMENT.md +++ b/content-gen/docs/AZD_DEPLOYMENT.md @@ -81,7 +81,7 @@ The deployment has sensible defaults, but you can customize: azd env set AZURE_LOCATION swedencentral # Set AI Services region (must support your models) -azd env set azureAiServiceLocation swedencentral +azd env set AZURE_ENV_OPENAI_LOCATION swedencentral # GPT Model configuration azd env set gptModelName gpt-4o @@ -157,21 +157,21 @@ This single command will: ```bash # Set the resource ID of your existing AI Project -azd env set azureExistingAIProjectResourceId "/subscriptions//resourceGroups//providers/Microsoft.MachineLearningServices/workspaces/" +azd env set AZURE_EXISTING_AI_PROJECT_RESOURCE_ID "/subscriptions//resourceGroups//providers/Microsoft.MachineLearningServices/workspaces/" ``` ### Reuse Existing Log Analytics Workspace ```bash # Set the resource ID of your existing Log Analytics workspace -azd env set existingLogAnalyticsWorkspaceId "/subscriptions//resourceGroups//providers/Microsoft.OperationalInsights/workspaces/" +azd env set AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID "/subscriptions//resourceGroups//providers/Microsoft.OperationalInsights/workspaces/" ``` ### Use Existing Container Registry ```bash # Set the name of your existing ACR -azd env set acrName myexistingacr +azd env set ACR_NAME myexistingacr ``` ## Post-Deployment @@ -312,7 +312,7 @@ Error: The model 'gpt-4o' is not available in region 'westeurope' **Solution**: Set a different region for AI Services: ```bash -azd env set azureAiServiceLocation eastus +azd env set AZURE_ENV_OPENAI_LOCATION eastus ``` #### 3. Container Build Fails diff --git a/content-gen/docs/CustomizingAzdParameters.md b/content-gen/docs/CustomizingAzdParameters.md new file mode 100644 index 000000000..4198278e8 --- /dev/null +++ b/content-gen/docs/CustomizingAzdParameters.md @@ -0,0 +1,43 @@ +## [Optional]: Customizing resource names + +By default this template will use the environment name as the prefix to prevent naming collisions within Azure. The parameters below show the default values. You only need to run the statements below if you need to change the values. + +> To override any of the parameters, run `azd env set ` before running `azd up`. On the first azd command, it will prompt you for the environment name. Be sure to choose 3-15 characters alphanumeric unique name. + +## Parameters + +| Name | Type | Default Value | Purpose | +| -------------------------------------- | ------- | ---------------------------- | ----------------------------------------------------------------------------- | +| `AZURE_LOCATION` | string | `` | Sets the Azure region for resource deployment. Allowed: `australiaeast`, `centralus`, `eastasia`, `eastus`, `eastus2`, `japaneast`, `northeurope`, `southeastasia`, `swedencentral`, `uksouth`, `westus`, `westus3`. | +| `AZURE_ENV_NAME` | string | `contentgen` | Sets the environment name prefix for all Azure resources (3-15 characters). | +| `secondaryLocation` | string | `uksouth` | Specifies a secondary Azure region for database creation. | +| `gptModelName` | string | `gpt-5.1` | Specifies the GPT model name to deploy. | +| `gptModelVersion` | string | `2025-11-13` | Sets the GPT model version. | +| `gptModelDeploymentType` | string | `GlobalStandard` | Defines the model deployment type (allowed: `Standard`, `GlobalStandard`). | +| `gptModelCapacity` | integer | `150` | Sets the GPT model token capacity (minimum: `10`). | +| `imageModelChoice` | string | `gpt-image-1` | Image model to deploy (allowed: `gpt-image-1`, `gpt-image-1.5`, `dall-e-3`, `none`). | +| `dalleModelCapacity` | integer | `1` | Sets the image model deployment capacity in RPM (minimum: `1`). | +| `azureOpenaiAPIVersion` | string | `2025-01-01-preview` | Specifies the API version for Azure OpenAI service. | +| `AZURE_ENV_OPENAI_LOCATION` | string | `` | Sets the Azure region for OpenAI resource deployment. | +| `AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID` | string | `""` | Reuses an existing Log Analytics Workspace instead of creating a new one. | +| `AZURE_EXISTING_AI_PROJECT_RESOURCE_ID`| string | `""` | Reuses an existing AI Foundry Project instead of creating a new one. | +| `ACR_NAME` | string | `contentgencontainerreg` | Sets the existing Azure Container Registry name (without `.azurecr.io`). | +| `IMAGE_TAG` | string | `latest` | Sets the container image tag (e.g., `latest`, `dev`, `hotfix`). | + +## How to Set a Parameter + +To customize any of the above values, run the following command **before** `azd up`: + +```bash +azd env set +``` + +**Examples:** + +```bash +azd env set AZURE_LOCATION westus2 +azd env set gptModelName gpt-5.1 +azd env set gptModelDeploymentType Standard +azd env set imageModelChoice dall-e-3 +azd env set ACR_NAME contentgencontainerreg +``` diff --git a/content-gen/infra/main.parameters.json b/content-gen/infra/main.parameters.json index 051635a50..9370c6250 100644 --- a/content-gen/infra/main.parameters.json +++ b/content-gen/infra/main.parameters.json @@ -8,6 +8,9 @@ "location": { "value": "${AZURE_LOCATION}" }, + "secondaryLocation": { + "value": "${secondaryLocation}" + }, "gptModelName": { "value": "${gptModelName}" }, @@ -36,31 +39,19 @@ "value": "${azureOpenaiAPIVersion}" }, "azureAiServiceLocation": { - "value": "${azureAiServiceLocation}" + "value": "${AZURE_ENV_OPENAI_LOCATION}" }, "existingLogAnalyticsWorkspaceId": { - "value": "${existingLogAnalyticsWorkspaceId}" + "value": "${AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID}" }, "azureExistingAIProjectResourceId": { - "value": "${azureExistingAIProjectResourceId}" + "value": "${AZURE_EXISTING_AI_PROJECT_RESOURCE_ID}" }, "acrName": { - "value": "${acrName}" + "value": "${ACR_NAME}" }, "imageTag": { - "value": "${imageTag=latest}" - }, - "enablePrivateNetworking": { - "value": "${enablePrivateNetworking}" - }, - "enableMonitoring": { - "value": "${enableMonitoring}" - }, - "enableScalability": { - "value": "${enableScalability}" - }, - "enableRedundancy": { - "value": "${enableRedundancy}" + "value": "${IMAGE_TAG=latest}" } } } diff --git a/content-gen/infra/main.waf.parameters.json b/content-gen/infra/main.waf.parameters.json new file mode 100644 index 000000000..d5d8438a2 --- /dev/null +++ b/content-gen/infra/main.waf.parameters.json @@ -0,0 +1,66 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "solutionName": { + "value": "${AZURE_ENV_NAME}" + }, + "location": { + "value": "${AZURE_LOCATION}" + }, + "secondaryLocation": { + "value": "${secondaryLocation}" + }, + "gptModelName": { + "value": "${gptModelName}" + }, + "gptModelVersion": { + "value": "${gptModelVersion}" + }, + "gptModelDeploymentType": { + "value": "${gptModelDeploymentType}" + }, + "gptModelCapacity": { + "value": "${gptModelCapacity}" + }, + "imageModelChoice": { + "value": "${imageModelChoice}" + }, + "dalleModelCapacity": { + "value": "${dalleModelCapacity}" + }, + "embeddingModel": { + "value": "${embeddingModel}" + }, + "embeddingDeploymentCapacity": { + "value": "${embeddingDeploymentCapacity}" + }, + "azureOpenaiAPIVersion": { + "value": "${azureOpenaiAPIVersion}" + }, + "azureAiServiceLocation": { + "value": "${AZURE_ENV_OPENAI_LOCATION}" + }, + "existingLogAnalyticsWorkspaceId": { + "value": "${AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID}" + }, + "azureExistingAIProjectResourceId": { + "value": "${AZURE_EXISTING_AI_PROJECT_RESOURCE_ID}" + }, + "acrName": { + "value": "${ACR_NAME}" + }, + "imageTag": { + "value": "${IMAGE_TAG=latest}" + }, + "enablePrivateNetworking": { + "value": true + }, + "enableMonitoring": { + "value": true + }, + "enableScalability": { + "value": true + } + } +} diff --git a/content-gen/scripts/checkquota.sh b/content-gen/scripts/checkquota.sh new file mode 100644 index 000000000..9a798b297 --- /dev/null +++ b/content-gen/scripts/checkquota.sh @@ -0,0 +1,196 @@ +#!/bin/bash + +# ============================================================================= +# Quota Check Script for Content Generation Solution Accelerator +# Checks Azure OpenAI quota across regions for GPT and image models. +# Selects the first region with sufficient quota for ALL required models. +# +# Works in both CI (service principal) and local (existing az login) modes. +# Auto-detects mode based on environment variables. +# +# Usage (local): +# bash checkquota.sh [image_model_choice] +# bash checkquota.sh gpt-image-1 +# bash checkquota.sh dall-e-3 +# bash checkquota.sh none +# +# Usage (CI - via env vars): +# Set AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID, +# AZURE_SUBSCRIPTION_ID, GPT_MIN_CAPACITY, AZURE_REGIONS, IMAGE_MODEL_CHOICE +# ============================================================================= + +# ---- Determine run mode: CI (service principal) or Local (existing session) ---- +if [[ -n "$AZURE_CLIENT_ID" && -n "$AZURE_CLIENT_SECRET" && -n "$AZURE_TENANT_ID" ]]; then + RUN_MODE="ci" +else + RUN_MODE="local" +fi + +# ---- Configuration ---- +# In local mode, image model can be passed as first argument +if [[ "$RUN_MODE" == "local" ]]; then + IMAGE_MODEL_CHOICE="${1:-${IMAGE_MODEL_CHOICE:-gpt-image-1}}" +else + IMAGE_MODEL_CHOICE="${IMAGE_MODEL_CHOICE:-gpt-image-1}" +fi + +GPT_MIN_CAPACITY="${GPT_MIN_CAPACITY:-150}" +IMAGE_MODEL_MIN_CAPACITY="${IMAGE_MODEL_MIN_CAPACITY:-1}" + +# Regions to check +if [[ -n "$AZURE_REGIONS" ]]; then + IFS=', ' read -ra REGIONS <<< "$AZURE_REGIONS" +else + REGIONS=("westus3" "eastus2" "uaenorth" "swedencentral" "australiaeast" "eastus" "uksouth" "japaneast") +fi + +# Map image model choice to Azure quota model name +declare -A IMAGE_MODEL_QUOTA_NAME +IMAGE_MODEL_QUOTA_NAME=( + ["gpt-image-1"]="OpenAI.GlobalStandard.gpt-image-1" + ["gpt-image-1.5"]="OpenAI.GlobalStandard.gpt-image-1.5" + ["dall-e-3"]="OpenAI.Standard.dall-e-3" + ["none"]="" +) + +# ---- Validate image model choice ---- +ALLOWED_MODELS=("gpt-image-1" "gpt-image-1.5" "dall-e-3" "none") +if [[ ! " ${ALLOWED_MODELS[@]} " =~ " ${IMAGE_MODEL_CHOICE} " ]]; then + echo "❌ ERROR: Invalid image model choice: '$IMAGE_MODEL_CHOICE'" + echo " Allowed values: ${ALLOWED_MODELS[*]}" + exit 1 +fi + +# ---- Authentication ---- +if [[ "$RUN_MODE" == "ci" ]]; then + echo "🔑 Authenticating using Service Principal (CI mode)..." + if ! az login --service-principal -u "$AZURE_CLIENT_ID" -p "$AZURE_CLIENT_SECRET" --tenant "$AZURE_TENANT_ID"; then + echo "❌ Error: Failed to login using Service Principal." + exit 1 + fi + + SUBSCRIPTION_ID="${AZURE_SUBSCRIPTION_ID}" + if [[ -z "$SUBSCRIPTION_ID" || -z "$GPT_MIN_CAPACITY" ]]; then + echo "❌ ERROR: Missing required environment variables." + echo " AZURE_SUBSCRIPTION_ID=${SUBSCRIPTION_ID:-(empty)}" + echo " GPT_MIN_CAPACITY=${GPT_MIN_CAPACITY:-(empty)}" + echo " AZURE_REGIONS=${AZURE_REGIONS:-(empty)}" + exit 1 + fi + + echo "🔄 Setting Azure subscription..." + if ! az account set --subscription "$SUBSCRIPTION_ID"; then + echo "❌ ERROR: Invalid subscription ID or insufficient permissions." + exit 1 + fi + echo "✅ Azure subscription set successfully." +else + echo "🔑 Using existing Azure CLI session (local mode)..." + if ! az account show &>/dev/null; then + echo "❌ Not logged in. Run 'az login' first." + exit 1 + fi + SUBSCRIPTION=$(az account show --query "name" -o tsv) + echo "✅ Using subscription: $SUBSCRIPTION" +fi + +echo "" +echo "📋 Configuration:" +echo " Mode: $RUN_MODE" +echo " Image Model Choice: $IMAGE_MODEL_CHOICE" +echo " GPT Min Capacity: $GPT_MIN_CAPACITY" +echo " Image Model Min Capacity: $IMAGE_MODEL_MIN_CAPACITY" +echo " Regions to check: ${REGIONS[*]}" +echo "" + +# ---- Build Model Capacity Map ---- +declare -A MIN_CAPACITY +MIN_CAPACITY=( + ["OpenAI.GlobalStandard.gpt-5.1"]=$GPT_MIN_CAPACITY +) + +# Add image model to quota check if not 'none' +IMAGE_QUOTA_NAME="${IMAGE_MODEL_QUOTA_NAME[$IMAGE_MODEL_CHOICE]}" +if [[ -n "$IMAGE_QUOTA_NAME" ]]; then + MIN_CAPACITY["$IMAGE_QUOTA_NAME"]=$IMAGE_MODEL_MIN_CAPACITY + echo "đŸ–ŧī¸ Image model '$IMAGE_MODEL_CHOICE' added to quota check (key: $IMAGE_QUOTA_NAME, min capacity: $IMAGE_MODEL_MIN_CAPACITY)" +else + echo "â„šī¸ Image model set to 'none' — skipping image model quota check." +fi +echo "" + +# ---- Main Quota Check Loop ---- +VALID_REGION="" +for REGION in "${REGIONS[@]}"; do + echo "========================================" + echo "🔍 Checking region: $REGION" + + QUOTA_INFO=$(az cognitiveservices usage list --location "$REGION" --output json 2>/dev/null) + if [ -z "$QUOTA_INFO" ]; then + echo " âš ī¸ Failed to retrieve quota for region $REGION. Skipping." + continue + fi + + INSUFFICIENT_QUOTA=false + for MODEL in "${!MIN_CAPACITY[@]}"; do + MODEL_INFO=$(echo "$QUOTA_INFO" | awk -v model="\"value\": \"$MODEL\"" ' + BEGIN { RS="},"; FS="," } + $0 ~ model { print $0 } + ') + + if [ -z "$MODEL_INFO" ]; then + echo " âš ī¸ No quota info for: $MODEL in $REGION. Skipping." + INSUFFICIENT_QUOTA=true + continue + fi + + CURRENT_VALUE=$(echo "$MODEL_INFO" | awk -F': ' '/"currentValue"/ {print $2}' | tr -d ',' | tr -d ' ') + LIMIT=$(echo "$MODEL_INFO" | awk -F': ' '/"limit"/ {print $2}' | tr -d ',' | tr -d ' ') + + CURRENT_VALUE=${CURRENT_VALUE:-0} + LIMIT=${LIMIT:-0} + + CURRENT_VALUE=$(echo "$CURRENT_VALUE" | cut -d'.' -f1) + LIMIT=$(echo "$LIMIT" | cut -d'.' -f1) + + AVAILABLE=$((LIMIT - CURRENT_VALUE)) + + if [ "$AVAILABLE" -lt "${MIN_CAPACITY[$MODEL]}" ]; then + echo " ❌ $MODEL | Used: $CURRENT_VALUE | Limit: $LIMIT | Available: $AVAILABLE | Need: ${MIN_CAPACITY[$MODEL]}" + INSUFFICIENT_QUOTA=true + break + else + echo " ✅ $MODEL | Used: $CURRENT_VALUE | Limit: $LIMIT | Available: $AVAILABLE | Need: ${MIN_CAPACITY[$MODEL]}" + fi + done + + if [ "$INSUFFICIENT_QUOTA" = false ]; then + VALID_REGION="$REGION" + echo " 🎉 Region '$REGION' has sufficient quota for all models!" + break + fi + +done + +echo "" +echo "========================================" +if [ -z "$VALID_REGION" ]; then + echo "❌ No region with sufficient quota found!" + echo " Image Model: $IMAGE_MODEL_CHOICE" + echo " Checked regions: ${REGIONS[*]}" + + # In CI mode, set GITHUB_ENV variable instead of exiting with error + if [[ "$RUN_MODE" == "ci" ]]; then + echo "QUOTA_FAILED=true" >> "$GITHUB_ENV" + exit 0 + else + exit 1 + fi +else + echo "✅ Recommended Region: $VALID_REGION" + + if [[ "$RUN_MODE" == "ci" ]]; then + echo "VALID_REGION=$VALID_REGION" >> "$GITHUB_ENV" + fi + exit 0 +fi diff --git a/content-gen/src/backend/orchestrator.py b/content-gen/src/backend/orchestrator.py index 7677ca362..3e90b0471 100644 --- a/content-gen/src/backend/orchestrator.py +++ b/content-gen/src/backend/orchestrator.py @@ -1245,7 +1245,14 @@ async def _generate_foundry_image(self, image_prompt: str, results: dict) -> Non # The direct image API endpoint image_api_url = f"{image_endpoint}/openai/deployments/{image_deployment}/images/generations" - api_version = app_settings.azure_openai.image_api_version or "2025-04-01-preview" + + # Adapt API version and payload to the deployed image model + is_dalle3 = image_deployment.lower().startswith("dall-e") + + if is_dalle3: + api_version = app_settings.azure_openai.preview_api_version or "2024-02-01" + else: + api_version = app_settings.azure_openai.image_api_version or "2025-04-01-preview" logger.info(f"Calling Foundry direct image API: {image_api_url}") logger.info(f"Prompt: {image_prompt[:200]}...") @@ -1255,13 +1262,24 @@ async def _generate_foundry_image(self, image_prompt: str, results: dict) -> Non "Content-Type": "application/json", } - # gpt-image-1 parameters (no response_format parameter) - payload = { - "prompt": image_prompt, - "n": 1, - "size": "1024x1024", - "quality": "medium", # gpt-image-1 uses low/medium/high/auto - } + # Build model-appropriate payload + if is_dalle3: + # dall-e-3: quality must be "standard" or "hd"; needs response_format; 4000-char prompt limit + payload = { + "prompt": image_prompt[:4000], + "n": 1, + "size": app_settings.azure_openai.image_size or "1024x1024", + "quality": app_settings.azure_openai.image_quality or "hd", + "response_format": "b64_json", + } + else: + # gpt-image-1 / gpt-image-1.5: quality is low/medium/high/auto; no response_format + payload = { + "prompt": image_prompt, + "n": 1, + "size": "1024x1024", + "quality": "medium", + } async with httpx.AsyncClient(timeout=120.0) as client: response = await client.post(