deploy-azd-core (reusable) #46
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: deploy-azd | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| environment: | |
| description: Deployment environment name (for azd -e) | |
| required: true | |
| default: dev | |
| location: | |
| description: Azure location | |
| required: true | |
| default: centralus | |
| projectName: | |
| description: Project prefix used by naming convention | |
| required: true | |
| default: holidaypeakhub | |
| imageTag: | |
| description: Image tag to deploy | |
| required: true | |
| default: latest | |
| deployStatic: | |
| description: Provision static web app resources | |
| required: true | |
| type: boolean | |
| default: true | |
| uiOnly: | |
| description: Deploy only the UI using SWA token (skips provision and Azure login jobs) | |
| required: true | |
| type: boolean | |
| default: false | |
| apiBaseUrl: | |
| description: Optional API base URL for UI build override | |
| required: false | |
| default: '' | |
| seedDemoData: | |
| description: Run demo faker seed job after deployment (non-prod only) | |
| required: true | |
| type: boolean | |
| default: true | |
| deployChangedOnly: | |
| description: Legacy flag (changed-app-only deployment is always enforced) | |
| required: true | |
| type: boolean | |
| default: true | |
| permissions: | |
| id-token: write | |
| contents: read | |
| jobs: | |
| detect-changes: | |
| if: ${{ !inputs.uiOnly }} | |
| runs-on: ubuntu-latest | |
| outputs: | |
| crud_changed: ${{ steps.detect.outputs.crud_changed }} | |
| ui_changed: ${{ steps.detect.outputs.ui_changed }} | |
| agents_changed: ${{ steps.detect.outputs.agents_changed }} | |
| changed_agents_matrix: ${{ steps.detect.outputs.changed_agents_matrix }} | |
| changed_agent_services_csv: ${{ steps.detect.outputs.changed_agent_services_csv }} | |
| changed_aks_services_csv: ${{ steps.detect.outputs.changed_aks_services_csv }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Detect changed services | |
| id: detect | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| DEFAULT_BRANCH="${{ github.event.repository.default_branch }}" | |
| git fetch origin "$DEFAULT_BRANCH" --depth=1 | |
| CHANGED_FILES=$(git diff --name-only "origin/$DEFAULT_BRANCH...HEAD") | |
| mapfile -t AGENT_SERVICES < <(python3 - <<'PY' | |
| import re | |
| with open('azure.yaml', encoding='utf-8') as f: | |
| lines = f.readlines() | |
| in_services = False | |
| current_service = None | |
| current_host = None | |
| services = [] | |
| for raw in lines: | |
| line = raw.rstrip('\n') | |
| if not in_services: | |
| if re.match(r'^services:\s*$', line): | |
| in_services = True | |
| continue | |
| if re.match(r'^[^\s]', line): | |
| break | |
| service_match = re.match(r'^ ([a-z0-9\-]+):\s*$', line) | |
| if service_match: | |
| if current_service and current_host == 'aks' and current_service != 'crud-service': | |
| services.append(current_service) | |
| current_service = service_match.group(1) | |
| current_host = None | |
| continue | |
| host_match = re.match(r'^ host:\s*(\S+)\s*$', line) | |
| if host_match: | |
| current_host = host_match.group(1) | |
| if current_service and current_host == 'aks' and current_service != 'crud-service': | |
| services.append(current_service) | |
| for service in services: | |
| print(service) | |
| PY | |
| ) | |
| CRUD_CHANGED=false | |
| if echo "$CHANGED_FILES" | grep -Eq '^apps/crud-service/'; then | |
| CRUD_CHANGED=true | |
| fi | |
| UI_CHANGED=false | |
| if echo "$CHANGED_FILES" | grep -Eq '^apps/ui/'; then | |
| UI_CHANGED=true | |
| fi | |
| AGENTS_CHANGED=false | |
| MATRIX='[]' | |
| CHANGED_AGENTS=() | |
| for service in "${AGENT_SERVICES[@]}"; do | |
| if echo "$CHANGED_FILES" | grep -Eq "^apps/$service/"; then | |
| CHANGED_AGENTS+=("$service") | |
| fi | |
| done | |
| if [ ${#CHANGED_AGENTS[@]} -gt 0 ]; then | |
| AGENTS_CHANGED=true | |
| MATRIX=$(printf '%s\n' "${CHANGED_AGENTS[@]}" | jq -R . | jq -s 'map({service: .})') | |
| fi | |
| CHANGED_AGENT_SERVICES_CSV="" | |
| if [ ${#CHANGED_AGENTS[@]} -gt 0 ]; then | |
| CHANGED_AGENT_SERVICES_CSV=$(IFS=,; echo "${CHANGED_AGENTS[*]}") | |
| fi | |
| CHANGED_AKS_SERVICES_CSV="$CHANGED_AGENT_SERVICES_CSV" | |
| if [ "$CRUD_CHANGED" = true ]; then | |
| if [ -n "$CHANGED_AKS_SERVICES_CSV" ]; then | |
| CHANGED_AKS_SERVICES_CSV="crud-service,$CHANGED_AKS_SERVICES_CSV" | |
| else | |
| CHANGED_AKS_SERVICES_CSV="crud-service" | |
| fi | |
| fi | |
| echo "crud_changed=$CRUD_CHANGED" >> "$GITHUB_OUTPUT" | |
| echo "ui_changed=$UI_CHANGED" >> "$GITHUB_OUTPUT" | |
| echo "agents_changed=$AGENTS_CHANGED" >> "$GITHUB_OUTPUT" | |
| echo "changed_agents_matrix=$MATRIX" >> "$GITHUB_OUTPUT" | |
| echo "changed_agent_services_csv=$CHANGED_AGENT_SERVICES_CSV" >> "$GITHUB_OUTPUT" | |
| echo "changed_aks_services_csv=$CHANGED_AKS_SERVICES_CSV" >> "$GITHUB_OUTPUT" | |
| provision: | |
| if: ${{ !inputs.uiOnly }} | |
| needs: detect-changes | |
| runs-on: ubuntu-latest | |
| environment: ${{ inputs.environment }} | |
| outputs: | |
| AI_SERVICES_NAME: ${{ steps.outputs.outputs.AI_SERVICES_NAME }} | |
| PROJECT_ENDPOINT: ${{ steps.outputs.outputs.PROJECT_ENDPOINT }} | |
| PROJECT_NAME: ${{ steps.outputs.outputs.PROJECT_NAME }} | |
| COSMOS_ACCOUNT_URI: ${{ steps.outputs.outputs.COSMOS_ACCOUNT_URI }} | |
| COSMOS_DATABASE: ${{ steps.outputs.outputs.COSMOS_DATABASE }} | |
| KEY_VAULT_URI: ${{ steps.outputs.outputs.KEY_VAULT_URI }} | |
| REDIS_HOST: ${{ steps.outputs.outputs.REDIS_HOST }} | |
| EVENT_HUB_NAMESPACE: ${{ steps.outputs.outputs.EVENT_HUB_NAMESPACE }} | |
| APPLICATIONINSIGHTS_CONNECTION_STRING: ${{ steps.outputs.outputs.APPLICATIONINSIGHTS_CONNECTION_STRING }} | |
| POSTGRES_HOST: ${{ steps.outputs.outputs.POSTGRES_HOST }} | |
| POSTGRES_USER: ${{ steps.outputs.outputs.POSTGRES_USER }} | |
| POSTGRES_DATABASE: ${{ steps.outputs.outputs.POSTGRES_DATABASE }} | |
| env: | |
| AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} | |
| AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} | |
| AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Azure login (OIDC) | |
| uses: azure/login@v2 | |
| with: | |
| client-id: ${{ env.AZURE_CLIENT_ID }} | |
| tenant-id: ${{ env.AZURE_TENANT_ID }} | |
| subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }} | |
| - name: Setup azd | |
| uses: Azure/setup-azd@v2 | |
| - name: Authenticate azd (OIDC) | |
| run: | | |
| azd auth login \ | |
| --client-id "${AZURE_CLIENT_ID}" \ | |
| --tenant-id "${AZURE_TENANT_ID}" \ | |
| --federated-credential-provider github \ | |
| --no-prompt | |
| - name: Configure azd environment context | |
| run: | | |
| azd env set AZURE_SUBSCRIPTION_ID "${AZURE_SUBSCRIPTION_ID}" -e "${{ inputs.environment }}" | |
| azd env set AZURE_LOCATION "${{ inputs.location }}" -e "${{ inputs.environment }}" | |
| azd env set AZURE_ENV_NAME "${{ inputs.environment }}" -e "${{ inputs.environment }}" | |
| azd env set AZURE_RESOURCE_GROUP "${{ inputs.projectName }}-${{ inputs.environment }}-rg" -e "${{ inputs.environment }}" | |
| azd env set resourceGroupName "${{ inputs.projectName }}-${{ inputs.environment }}-rg" -e "${{ inputs.environment }}" | |
| azd env set AZURE_AKS_CLUSTER_NAME "${{ inputs.projectName }}-${{ inputs.environment }}-aks" -e "${{ inputs.environment }}" | |
| azd env set AKS_CLUSTER_NAME "${{ inputs.projectName }}-${{ inputs.environment }}-aks" -e "${{ inputs.environment }}" | |
| azd env set AZURE_CONTAINER_REGISTRY_ENDPOINT "${{ inputs.projectName }}${{ inputs.environment }}acr.azurecr.io" -e "${{ inputs.environment }}" | |
| - name: Install kubelogin | |
| run: | | |
| for attempt in 1 2 3 4 5; do | |
| if az aks install-cli --kubelogin-version latest; then | |
| exit 0 | |
| fi | |
| echo "kubelogin install failed on attempt ${attempt}; retrying..." | |
| sleep 15 | |
| done | |
| echo "kubelogin install failed after retries" | |
| exit 1 | |
| - name: Ensure hook scripts executable | |
| run: chmod +x ./.infra/azd/hooks/*.sh | |
| - name: Ensure AKS cluster is running | |
| run: | | |
| AKS_RG="${{ inputs.projectName }}-${{ inputs.environment }}-rg" | |
| AKS_NAME="${{ inputs.projectName }}-${{ inputs.environment }}-aks" | |
| if az aks show --resource-group "$AKS_RG" --name "$AKS_NAME" >/dev/null 2>&1; then | |
| POWER_STATE=$(az aks show --resource-group "$AKS_RG" --name "$AKS_NAME" --query "powerState.code" -o tsv 2>/dev/null || true) | |
| if [ "$POWER_STATE" = "Stopped" ]; then | |
| echo "AKS cluster is stopped. Starting $AKS_NAME..." | |
| az aks start --resource-group "$AKS_RG" --name "$AKS_NAME" | |
| for _ in $(seq 1 40); do | |
| CURRENT_STATE=$(az aks show --resource-group "$AKS_RG" --name "$AKS_NAME" --query "powerState.code" -o tsv 2>/dev/null || true) | |
| if [ "$CURRENT_STATE" = "Running" ]; then | |
| echo "AKS cluster is running." | |
| break | |
| fi | |
| sleep 30 | |
| done | |
| fi | |
| else | |
| echo "AKS cluster does not exist yet. Continuing." | |
| fi | |
| - name: Install kubelogin | |
| run: | | |
| for attempt in 1 2 3 4 5; do | |
| if az aks install-cli --kubelogin-version latest; then | |
| exit 0 | |
| fi | |
| echo "kubelogin install failed on attempt ${attempt}; retrying..." | |
| sleep 15 | |
| done | |
| echo "kubelogin install failed after retries" | |
| exit 1 | |
| - name: Ensure hook scripts executable | |
| run: chmod +x ./.infra/azd/hooks/*.sh | |
| - name: Configure azd environment | |
| run: | | |
| azd env set AZURE_SUBSCRIPTION_ID "${AZURE_SUBSCRIPTION_ID}" -e "${{ inputs.environment }}" | |
| azd env set AZURE_LOCATION "${{ inputs.location }}" -e "${{ inputs.environment }}" | |
| azd env set AZURE_ENV_NAME "${{ inputs.environment }}" -e "${{ inputs.environment }}" | |
| azd env set AZURE_RESOURCE_GROUP "${{ inputs.projectName }}-${{ inputs.environment }}-rg" -e "${{ inputs.environment }}" | |
| azd env set resourceGroupName "${{ inputs.projectName }}-${{ inputs.environment }}-rg" -e "${{ inputs.environment }}" | |
| azd env set AZURE_AKS_CLUSTER_NAME "${{ inputs.projectName }}-${{ inputs.environment }}-aks" -e "${{ inputs.environment }}" | |
| azd env set AKS_CLUSTER_NAME "${{ inputs.projectName }}-${{ inputs.environment }}-aks" -e "${{ inputs.environment }}" | |
| azd env set AZURE_CONTAINER_REGISTRY_ENDPOINT "${{ inputs.projectName }}${{ inputs.environment }}acr.azurecr.io" -e "${{ inputs.environment }}" | |
| azd env set deployShared true -e "${{ inputs.environment }}" | |
| azd env set deployStatic "${{ inputs.deployStatic }}" -e "${{ inputs.environment }}" | |
| azd env set environment "${{ inputs.environment }}" -e "${{ inputs.environment }}" | |
| azd env set location "${{ inputs.location }}" -e "${{ inputs.environment }}" | |
| azd env set projectName "${{ inputs.projectName }}" -e "${{ inputs.environment }}" | |
| azd env set IMAGE_PREFIX "ghcr.io/${{ github.repository_owner }}" -e "${{ inputs.environment }}" | |
| azd env set IMAGE_TAG "${{ inputs.imageTag }}" -e "${{ inputs.environment }}" | |
| azd env set K8S_NAMESPACE holiday-peak -e "${{ inputs.environment }}" | |
| azd env set KEDA_ENABLED false -e "${{ inputs.environment }}" | |
| - name: Provision infrastructure | |
| run: azd provision --no-prompt -e "${{ inputs.environment }}" | |
| - name: Ensure AKS identity can read Key Vault secrets | |
| run: | | |
| AKS_RG="${{ inputs.projectName }}-${{ inputs.environment }}-rg" | |
| AKS_NAME="${{ inputs.projectName }}-${{ inputs.environment }}-aks" | |
| KEY_VAULT_NAME="${{ inputs.projectName }}-${{ inputs.environment }}-kv" | |
| KUBELET_OBJECT_ID=$(az aks show \ | |
| --resource-group "$AKS_RG" \ | |
| --name "$AKS_NAME" \ | |
| --query "identityProfile.kubeletidentity.objectId" -o tsv) | |
| KEY_VAULT_ID=$(az keyvault show \ | |
| --resource-group "$AKS_RG" \ | |
| --name "$KEY_VAULT_NAME" \ | |
| --query id -o tsv) | |
| EXISTING=$(az role assignment list \ | |
| --assignee-object-id "$KUBELET_OBJECT_ID" \ | |
| --scope "$KEY_VAULT_ID" \ | |
| --query "[?roleDefinitionName=='Key Vault Secrets User'] | length(@)" -o tsv) | |
| if [ "$EXISTING" = "0" ]; then | |
| az role assignment create \ | |
| --assignee-object-id "$KUBELET_OBJECT_ID" \ | |
| --assignee-principal-type ServicePrincipal \ | |
| --role "Key Vault Secrets User" \ | |
| --scope "$KEY_VAULT_ID" | |
| fi | |
| - name: Export provisioned outputs | |
| id: outputs | |
| run: | | |
| eval "$(azd env get-values -e '${{ inputs.environment }}' | sed 's/^/export /')" | |
| { | |
| echo "AI_SERVICES_NAME=${AI_SERVICES_NAME}" | |
| echo "PROJECT_ENDPOINT=${PROJECT_ENDPOINT}" | |
| echo "PROJECT_NAME=${PROJECT_NAME}" | |
| echo "COSMOS_ACCOUNT_URI=${COSMOS_ACCOUNT_URI}" | |
| echo "COSMOS_DATABASE=${COSMOS_DATABASE}" | |
| echo "KEY_VAULT_URI=${KEY_VAULT_URI}" | |
| echo "REDIS_HOST=${REDIS_HOST}" | |
| echo "EVENT_HUB_NAMESPACE=${EVENT_HUB_NAMESPACE}" | |
| echo "APPLICATIONINSIGHTS_CONNECTION_STRING=${APPLICATIONINSIGHTS_CONNECTION_STRING}" | |
| echo "POSTGRES_HOST=${POSTGRES_HOST}" | |
| echo "POSTGRES_USER=${POSTGRES_USER}" | |
| echo "POSTGRES_DATABASE=${POSTGRES_DATABASE}" | |
| } >> "$GITHUB_OUTPUT" | |
| deploy-foundry-models: | |
| runs-on: ubuntu-latest | |
| needs: provision | |
| environment: ${{ inputs.environment }} | |
| env: | |
| AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} | |
| AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} | |
| AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Azure login (OIDC) | |
| uses: azure/login@v2 | |
| with: | |
| client-id: ${{ env.AZURE_CLIENT_ID }} | |
| tenant-id: ${{ env.AZURE_TENANT_ID }} | |
| subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }} | |
| - name: Deploy AI model deployments | |
| run: | | |
| bash .infra/azd/hooks/deploy-foundry-models.sh \ | |
| "${{ inputs.projectName }}-${{ inputs.environment }}-rg" \ | |
| "${{ needs.provision.outputs.AI_SERVICES_NAME }}" | |
| env: | |
| AZURE_RESOURCE_GROUP: ${{ inputs.projectName }}-${{ inputs.environment }}-rg | |
| AI_SERVICES_NAME: ${{ needs.provision.outputs.AI_SERVICES_NAME }} | |
| deploy-crud: | |
| runs-on: ubuntu-latest | |
| needs: | |
| - provision | |
| - detect-changes | |
| if: ${{ !inputs.uiOnly && needs.detect-changes.outputs.crud_changed == 'true' }} | |
| environment: ${{ inputs.environment }} | |
| env: | |
| AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} | |
| AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} | |
| AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Azure login (OIDC) | |
| uses: azure/login@v2 | |
| with: | |
| client-id: ${{ env.AZURE_CLIENT_ID }} | |
| tenant-id: ${{ env.AZURE_TENANT_ID }} | |
| subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }} | |
| - name: Setup azd | |
| uses: Azure/setup-azd@v2 | |
| - name: Authenticate azd (OIDC) | |
| run: | | |
| azd auth login \ | |
| --client-id "${AZURE_CLIENT_ID}" \ | |
| --tenant-id "${AZURE_TENANT_ID}" \ | |
| --federated-credential-provider github \ | |
| --no-prompt | |
| - name: Configure azd environment context | |
| run: | | |
| azd env set AZURE_SUBSCRIPTION_ID "${AZURE_SUBSCRIPTION_ID}" -e "${{ inputs.environment }}" | |
| azd env set AZURE_LOCATION "${{ inputs.location }}" -e "${{ inputs.environment }}" | |
| azd env set AZURE_ENV_NAME "${{ inputs.environment }}" -e "${{ inputs.environment }}" | |
| azd env set AZURE_RESOURCE_GROUP "${{ inputs.projectName }}-${{ inputs.environment }}-rg" -e "${{ inputs.environment }}" | |
| azd env set resourceGroupName "${{ inputs.projectName }}-${{ inputs.environment }}-rg" -e "${{ inputs.environment }}" | |
| azd env set AZURE_AKS_CLUSTER_NAME "${{ inputs.projectName }}-${{ inputs.environment }}-aks" -e "${{ inputs.environment }}" | |
| azd env set AKS_CLUSTER_NAME "${{ inputs.projectName }}-${{ inputs.environment }}-aks" -e "${{ inputs.environment }}" | |
| azd env set AZURE_CONTAINER_REGISTRY_ENDPOINT "${{ inputs.projectName }}${{ inputs.environment }}acr.azurecr.io" -e "${{ inputs.environment }}" | |
| - name: Install kubelogin | |
| run: | | |
| for attempt in 1 2 3 4 5; do | |
| if az aks install-cli --kubelogin-version latest; then | |
| exit 0 | |
| fi | |
| echo "kubelogin install failed on attempt ${attempt}; retrying..." | |
| sleep 15 | |
| done | |
| echo "kubelogin install failed after retries" | |
| exit 1 | |
| - name: Ensure hook scripts executable | |
| run: chmod +x ./.infra/azd/hooks/*.sh | |
| - name: Configure azd environment context | |
| run: | | |
| azd env set AZURE_SUBSCRIPTION_ID "${AZURE_SUBSCRIPTION_ID}" -e "${{ inputs.environment }}" | |
| azd env set AZURE_LOCATION "${{ inputs.location }}" -e "${{ inputs.environment }}" | |
| azd env set AZURE_ENV_NAME "${{ inputs.environment }}" -e "${{ inputs.environment }}" | |
| azd env set AZURE_RESOURCE_GROUP "${{ inputs.projectName }}-${{ inputs.environment }}-rg" -e "${{ inputs.environment }}" | |
| azd env set resourceGroupName "${{ inputs.projectName }}-${{ inputs.environment }}-rg" -e "${{ inputs.environment }}" | |
| azd env set AZURE_AKS_CLUSTER_NAME "${{ inputs.projectName }}-${{ inputs.environment }}-aks" -e "${{ inputs.environment }}" | |
| azd env set AKS_CLUSTER_NAME "${{ inputs.projectName }}-${{ inputs.environment }}-aks" -e "${{ inputs.environment }}" | |
| azd env set AZURE_CONTAINER_REGISTRY_ENDPOINT "${{ inputs.projectName }}${{ inputs.environment }}acr.azurecr.io" -e "${{ inputs.environment }}" | |
| - name: Get AKS credentials | |
| run: | | |
| az aks get-credentials \ | |
| --resource-group "${{ inputs.projectName }}-${{ inputs.environment }}-rg" \ | |
| --name "${{ inputs.projectName }}-${{ inputs.environment }}-aks" \ | |
| --overwrite-existing | |
| - name: Resolve AKS managed identity client ID | |
| run: | | |
| AKS_MI_CLIENT_ID=$(az aks show \ | |
| --resource-group "${{ inputs.projectName }}-${{ inputs.environment }}-rg" \ | |
| --name "${{ inputs.projectName }}-${{ inputs.environment }}-aks" \ | |
| --query "identityProfile.kubeletidentity.clientId" -o tsv) | |
| echo "WORKLOAD_AZURE_CLIENT_ID=${AKS_MI_CLIENT_ID}" >> "$GITHUB_ENV" | |
| - name: Deploy CRUD service | |
| run: | | |
| if ! azd deploy --service crud-service --no-prompt -e "${{ inputs.environment }}"; then | |
| echo "Initial CRUD deploy failed; retrying once after short wait..." | |
| sleep 60 | |
| azd deploy --service crud-service --no-prompt -e "${{ inputs.environment }}" | |
| fi | |
| env: | |
| AZURE_CLIENT_ID: ${{ env.WORKLOAD_AZURE_CLIENT_ID }} | |
| AZURE_TENANT_ID: ${{ env.AZURE_TENANT_ID }} | |
| PROJECT_ENDPOINT: ${{ needs.provision.outputs.PROJECT_ENDPOINT }} | |
| PROJECT_NAME: ${{ needs.provision.outputs.PROJECT_NAME }} | |
| MODEL_DEPLOYMENT_NAME_FAST: gpt-5-nano | |
| MODEL_DEPLOYMENT_NAME_RICH: gpt-5-nano | |
| COSMOS_ACCOUNT_URI: ${{ needs.provision.outputs.COSMOS_ACCOUNT_URI }} | |
| COSMOS_DATABASE: ${{ needs.provision.outputs.COSMOS_DATABASE }} | |
| REDIS_HOST: ${{ needs.provision.outputs.REDIS_HOST }} | |
| EVENT_HUB_NAMESPACE: ${{ needs.provision.outputs.EVENT_HUB_NAMESPACE }} | |
| KEY_VAULT_URI: ${{ needs.provision.outputs.KEY_VAULT_URI }} | |
| APPLICATIONINSIGHTS_CONNECTION_STRING: ${{ needs.provision.outputs.APPLICATIONINSIGHTS_CONNECTION_STRING }} | |
| POSTGRES_HOST: ${{ needs.provision.outputs.POSTGRES_HOST }} | |
| POSTGRES_USER: ${{ needs.provision.outputs.POSTGRES_USER }} | |
| POSTGRES_DATABASE: ${{ needs.provision.outputs.POSTGRES_DATABASE }} | |
| deploy-ui: | |
| runs-on: ubuntu-latest | |
| if: ${{ inputs.deployStatic && !inputs.uiOnly && needs.detect-changes.outputs.ui_changed == 'true' }} | |
| needs: | |
| - provision | |
| - detect-changes | |
| environment: ${{ inputs.environment }} | |
| env: | |
| AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} | |
| AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} | |
| AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Azure login (OIDC) | |
| uses: azure/login@v2 | |
| with: | |
| client-id: ${{ env.AZURE_CLIENT_ID }} | |
| tenant-id: ${{ env.AZURE_TENANT_ID }} | |
| subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }} | |
| - name: Setup azd | |
| uses: Azure/setup-azd@v2 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '20' | |
| - name: Resolve API URL | |
| id: api | |
| run: | | |
| RESOURCE_GROUP="${{ inputs.projectName }}-${{ inputs.environment }}-rg" | |
| APIM_URL=$(az apim show \ | |
| --name "${{ inputs.projectName }}-${{ inputs.environment }}-apim" \ | |
| --resource-group "$RESOURCE_GROUP" \ | |
| --query "gatewayUrl" -o tsv) | |
| echo "api_url=$APIM_URL" >> "$GITHUB_OUTPUT" | |
| - name: Resolve SWA deployment token | |
| id: swa | |
| run: | | |
| RESOURCE_GROUP="${{ inputs.projectName }}-${{ inputs.environment }}-rg" | |
| SWA_NAME=$(az resource list \ | |
| --resource-group "$RESOURCE_GROUP" \ | |
| --resource-type "Microsoft.Web/staticSites" \ | |
| --query "[0].name" -o tsv) | |
| if [ -z "$SWA_NAME" ]; then | |
| echo "No Static Web App found in resource group $RESOURCE_GROUP" | |
| exit 1 | |
| fi | |
| SWA_TOKEN=$(az staticwebapp secrets list \ | |
| --name "$SWA_NAME" \ | |
| --resource-group "$RESOURCE_GROUP" \ | |
| --query "properties.apiKey" -o tsv) | |
| if [ -z "$SWA_TOKEN" ]; then | |
| echo "Failed to resolve deployment token for SWA $SWA_NAME" | |
| exit 1 | |
| fi | |
| echo "::add-mask::$SWA_TOKEN" | |
| echo "swa_name=$SWA_NAME" >> "$GITHUB_OUTPUT" | |
| echo "swa_token=$SWA_TOKEN" >> "$GITHUB_OUTPUT" | |
| - name: Deploy UI via Static Web Apps action | |
| uses: Azure/static-web-apps-deploy@v1 | |
| with: | |
| action: upload | |
| azure_static_web_apps_api_token: ${{ steps.swa.outputs.swa_token }} | |
| app_location: apps/ui | |
| output_location: '' | |
| skip_api_build: true | |
| app_build_command: yarn install --frozen-lockfile && yarn build | |
| env: | |
| NEXT_PUBLIC_API_URL: ${{ steps.api.outputs.api_url }} | |
| NEXT_PUBLIC_CRUD_API_URL: ${{ steps.api.outputs.api_url }} | |
| NEXT_PUBLIC_ENTRA_CLIENT_ID: ${{ secrets.NEXT_PUBLIC_ENTRA_CLIENT_ID }} | |
| NEXT_PUBLIC_ENTRA_TENANT_ID: ${{ secrets.NEXT_PUBLIC_ENTRA_TENANT_ID }} | |
| deploy-ui-token: | |
| runs-on: ubuntu-latest | |
| if: ${{ inputs.deployStatic && inputs.uiOnly }} | |
| environment: ${{ inputs.environment }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '20' | |
| - name: Resolve API URL | |
| id: api | |
| run: | | |
| if [ -n "${{ inputs.apiBaseUrl }}" ]; then | |
| echo "api_url=${{ inputs.apiBaseUrl }}" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "api_url=https://apim-${{ inputs.projectName }}-${{ inputs.environment }}.azure-api.net" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Deploy UI via Static Web Apps action (token mode) | |
| uses: Azure/static-web-apps-deploy@v1 | |
| with: | |
| action: upload | |
| azure_static_web_apps_api_token: ${{ secrets.SWA_DEPLOYMENT_TOKEN }} | |
| app_location: apps/ui | |
| output_location: '' | |
| skip_api_build: true | |
| app_build_command: yarn install --frozen-lockfile && yarn build | |
| env: | |
| NEXT_PUBLIC_API_URL: ${{ steps.api.outputs.api_url }} | |
| NEXT_PUBLIC_CRUD_API_URL: ${{ steps.api.outputs.api_url }} | |
| NEXT_PUBLIC_ENTRA_CLIENT_ID: ${{ secrets.NEXT_PUBLIC_ENTRA_CLIENT_ID }} | |
| NEXT_PUBLIC_ENTRA_TENANT_ID: ${{ secrets.NEXT_PUBLIC_ENTRA_TENANT_ID }} | |
| deploy-agents: | |
| runs-on: ubuntu-latest | |
| if: ${{ !inputs.uiOnly && needs.detect-changes.outputs.agents_changed == 'true' }} | |
| needs: | |
| - provision | |
| - deploy-crud | |
| - deploy-foundry-models | |
| - detect-changes | |
| environment: ${{ inputs.environment }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: ${{ fromJSON(needs.detect-changes.outputs.changed_agents_matrix) }} | |
| env: | |
| AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} | |
| AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} | |
| AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Azure login (OIDC) | |
| uses: azure/login@v2 | |
| with: | |
| client-id: ${{ env.AZURE_CLIENT_ID }} | |
| tenant-id: ${{ env.AZURE_TENANT_ID }} | |
| subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }} | |
| - name: Setup azd | |
| uses: Azure/setup-azd@v2 | |
| - name: Authenticate azd (OIDC) | |
| run: | | |
| azd auth login \ | |
| --client-id "${AZURE_CLIENT_ID}" \ | |
| --tenant-id "${AZURE_TENANT_ID}" \ | |
| --federated-credential-provider github \ | |
| --no-prompt | |
| - name: Get AKS credentials | |
| run: | | |
| az aks get-credentials \ | |
| --resource-group "${{ inputs.projectName }}-${{ inputs.environment }}-rg" \ | |
| --name "${{ inputs.projectName }}-${{ inputs.environment }}-aks" \ | |
| --overwrite-existing | |
| - name: Resolve AKS managed identity client ID | |
| run: | | |
| AKS_MI_CLIENT_ID=$(az aks show \ | |
| --resource-group "${{ inputs.projectName }}-${{ inputs.environment }}-rg" \ | |
| --name "${{ inputs.projectName }}-${{ inputs.environment }}-aks" \ | |
| --query "identityProfile.kubeletidentity.clientId" -o tsv) | |
| echo "WORKLOAD_AZURE_CLIENT_ID=${AKS_MI_CLIENT_ID}" >> "$GITHUB_ENV" | |
| - name: Deploy service | |
| run: azd deploy --service "${{ matrix.service }}" --no-prompt -e "${{ inputs.environment }}" | |
| env: | |
| AZURE_CLIENT_ID: ${{ env.WORKLOAD_AZURE_CLIENT_ID }} | |
| AZURE_TENANT_ID: ${{ env.AZURE_TENANT_ID }} | |
| PROJECT_ENDPOINT: ${{ needs.provision.outputs.PROJECT_ENDPOINT }} | |
| PROJECT_NAME: ${{ needs.provision.outputs.PROJECT_NAME }} | |
| FOUNDRY_AUTO_ENSURE_ON_STARTUP: "true" | |
| FOUNDRY_STRICT_ENFORCEMENT: "true" | |
| MODEL_DEPLOYMENT_NAME_FAST: gpt-5-nano | |
| MODEL_DEPLOYMENT_NAME_RICH: gpt-5-nano | |
| COSMOS_ACCOUNT_URI: ${{ needs.provision.outputs.COSMOS_ACCOUNT_URI }} | |
| COSMOS_DATABASE: ${{ needs.provision.outputs.COSMOS_DATABASE }} | |
| REDIS_HOST: ${{ needs.provision.outputs.REDIS_HOST }} | |
| EVENT_HUB_NAMESPACE: ${{ needs.provision.outputs.EVENT_HUB_NAMESPACE }} | |
| KEY_VAULT_URI: ${{ needs.provision.outputs.KEY_VAULT_URI }} | |
| APPLICATIONINSIGHTS_CONNECTION_STRING: ${{ needs.provision.outputs.APPLICATIONINSIGHTS_CONNECTION_STRING }} | |
| POSTGRES_HOST: ${{ needs.provision.outputs.POSTGRES_HOST }} | |
| POSTGRES_USER: ${{ needs.provision.outputs.POSTGRES_USER }} | |
| POSTGRES_DATABASE: ${{ needs.provision.outputs.POSTGRES_DATABASE }} | |
| sync-apim: | |
| runs-on: ubuntu-latest | |
| if: ${{ !inputs.uiOnly && (needs.detect-changes.outputs.agents_changed == 'true' || needs.detect-changes.outputs.crud_changed == 'true') && (needs.deploy-crud.result == 'success' || needs.deploy-crud.result == 'skipped') && (needs.deploy-agents.result == 'success' || needs.deploy-agents.result == 'skipped') }} | |
| needs: | |
| - deploy-crud | |
| - deploy-agents | |
| - detect-changes | |
| environment: ${{ inputs.environment }} | |
| env: | |
| AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} | |
| AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} | |
| AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} | |
| CHANGED_SERVICES: ${{ needs.detect-changes.outputs.changed_aks_services_csv }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Azure login (OIDC) | |
| uses: azure/login@v2 | |
| with: | |
| client-id: ${{ env.AZURE_CLIENT_ID }} | |
| tenant-id: ${{ env.AZURE_TENANT_ID }} | |
| subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }} | |
| - name: Get AKS credentials | |
| run: | | |
| az aks get-credentials \ | |
| --resource-group "${{ inputs.projectName }}-${{ inputs.environment }}-rg" \ | |
| --name "${{ inputs.projectName }}-${{ inputs.environment }}-aks" \ | |
| --overwrite-existing | |
| - name: Sync APIM agent APIs | |
| run: | | |
| bash .infra/azd/hooks/sync-apim-agents.sh --use-ingress \ | |
| --app-gw-name "${{ inputs.projectName }}-${{ inputs.environment }}-appgw" | |
| env: | |
| AZURE_RESOURCE_GROUP: ${{ inputs.projectName }}-${{ inputs.environment }}-rg | |
| ensure-foundry-agents: | |
| runs-on: ubuntu-latest | |
| if: ${{ !inputs.uiOnly && needs.detect-changes.outputs.agents_changed == 'true' && (needs.deploy-agents.result == 'success' || needs.deploy-agents.result == 'skipped') }} | |
| needs: | |
| - deploy-agents | |
| - detect-changes | |
| environment: ${{ inputs.environment }} | |
| env: | |
| AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} | |
| AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} | |
| AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} | |
| CHANGED_SERVICES: ${{ needs.detect-changes.outputs.changed_agent_services_csv }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Azure login (OIDC) | |
| uses: azure/login@v2 | |
| with: | |
| client-id: ${{ env.AZURE_CLIENT_ID }} | |
| tenant-id: ${{ env.AZURE_TENANT_ID }} | |
| subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }} | |
| - name: Setup Python (for azure.yaml parser) | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.13' | |
| - name: Get AKS credentials | |
| run: | | |
| az aks get-credentials \ | |
| --resource-group "${{ inputs.projectName }}-${{ inputs.environment }}-rg" \ | |
| --name "${{ inputs.projectName }}-${{ inputs.environment }}-aks" \ | |
| --overwrite-existing | |
| - name: Ensure all V2 Foundry agents are provisioned | |
| run: | | |
| bash .infra/azd/hooks/ensure-foundry-agents.sh --port-forward | |
| env: | |
| K8S_NAMESPACE: holiday-peak | |
| - name: Verify Foundry readiness across services | |
| run: | | |
| if [ -n "${CHANGED_SERVICES}" ]; then | |
| SERVICES=$(printf '%s' "${CHANGED_SERVICES}" | tr ',' '\n' | sed '/^crud-service$/d' | sed '/^$/d') | |
| else | |
| SERVICES=$(python3 -c " | |
| import re | |
| with open('azure.yaml') as f: lines = f.readlines() | |
| s, cs, ch = False, None, None | |
| svcs = [] | |
| for l in lines: | |
| l = l.rstrip() | |
| if not s: | |
| if re.match(r'^services:\s*$', l): s = True | |
| continue | |
| if re.match(r'^[^\s]', l): break | |
| m = re.match(r'^ ([a-z0-9\-]+):\s*$', l) | |
| if m: | |
| if cs and ch == 'aks' and cs != 'crud-service': svcs.append(cs) | |
| cs, ch = m.group(1), None | |
| continue | |
| h = re.match(r'^ host:\s*(\S+)', l) | |
| if h: ch = h.group(1) | |
| if cs and ch == 'aks' and cs != 'crud-service': svcs.append(cs) | |
| print('\\n'.join(svcs)) | |
| ") | |
| fi | |
| FAILED=0 | |
| echo "$SERVICES" | while IFS= read -r SVC; do | |
| [ -z "$SVC" ] && continue | |
| LOCAL_PORT=$(python3 -c 'import socket; s=socket.socket(); s.bind(("",0)); print(s.getsockname()[1]); s.close()') | |
| kubectl port-forward "svc/$SVC" "$LOCAL_PORT:8000" -n holiday-peak & | |
| PF_PID=$! | |
| sleep 3 | |
| STATUS=$(curl -s "http://localhost:$LOCAL_PORT/health" 2>/dev/null | python3 -c "import sys,json; print(json.load(sys.stdin).get('status','unknown'))" 2>/dev/null || echo "unreachable") | |
| kill "$PF_PID" 2>/dev/null || true | |
| if [ "$STATUS" = "ok" ]; then | |
| echo " [OK] $SVC" | |
| else | |
| echo " [WARN] $SVC: status=$STATUS" | |
| FAILED=$((FAILED + 1)) | |
| fi | |
| done | |
| if [ "$FAILED" -gt 0 ]; then | |
| echo "WARNING: $FAILED service(s) not fully ready." | |
| fi | |
| seed-demo-data: | |
| runs-on: ubuntu-latest | |
| needs: | |
| - sync-apim | |
| - ensure-foundry-agents | |
| - deploy-ui | |
| if: ${{ always() && inputs.seedDemoData && needs.ensure-foundry-agents.result == 'success' && (needs.deploy-ui.result == 'success' || needs.deploy-ui.result == 'skipped') && inputs.environment != 'prod' && inputs.environment != 'production' }} | |
| environment: ${{ inputs.environment }} | |
| env: | |
| AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} | |
| AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} | |
| AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Azure login (OIDC) | |
| uses: azure/login@v2 | |
| with: | |
| client-id: ${{ env.AZURE_CLIENT_ID }} | |
| tenant-id: ${{ env.AZURE_TENANT_ID }} | |
| subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }} | |
| - name: Setup azd | |
| uses: Azure/setup-azd@v2 | |
| - name: Resolve environment values | |
| run: | | |
| eval "$(azd env get-values -e '${{ inputs.environment }}' | sed 's/^/export /')" | |
| if [ -z "${SERVICE_CRUD_SERVICE_IMAGE_NAME:-}" ]; then | |
| echo "SERVICE_CRUD_SERVICE_IMAGE_NAME is required for seed job" | |
| exit 1 | |
| fi | |
| echo "CRUD_IMAGE=${SERVICE_CRUD_SERVICE_IMAGE_NAME}" >> "$GITHUB_ENV" | |
| echo "POSTGRES_HOST=${POSTGRES_HOST}" >> "$GITHUB_ENV" | |
| echo "POSTGRES_USER=${POSTGRES_USER}" >> "$GITHUB_ENV" | |
| echo "POSTGRES_PASSWORD=${POSTGRES_PASSWORD}" >> "$GITHUB_ENV" | |
| echo "POSTGRES_DATABASE=${POSTGRES_DATABASE}" >> "$GITHUB_ENV" | |
| echo "POSTGRES_PORT=${POSTGRES_PORT}" >> "$GITHUB_ENV" | |
| echo "POSTGRES_SSL=${POSTGRES_SSL}" >> "$GITHUB_ENV" | |
| - name: Get AKS credentials | |
| run: | | |
| az aks get-credentials \ | |
| --resource-group "${{ inputs.projectName }}-${{ inputs.environment }}-rg" \ | |
| --name "${{ inputs.projectName }}-${{ inputs.environment }}-aks" \ | |
| --overwrite-existing | |
| - name: Run demo faker seed job | |
| run: | | |
| JOB_NAME="crud-demo-seed-${{ github.run_id }}" | |
| cat <<EOF | kubectl apply -f - | |
| apiVersion: batch/v1 | |
| kind: Job | |
| metadata: | |
| name: ${JOB_NAME} | |
| namespace: holiday-peak | |
| spec: | |
| backoffLimit: 1 | |
| ttlSecondsAfterFinished: 600 | |
| template: | |
| spec: | |
| restartPolicy: Never | |
| containers: | |
| - name: seed | |
| image: ${CRUD_IMAGE} | |
| imagePullPolicy: Always | |
| command: ["python", "-m", "crud_service.scripts.seed_demo_data"] | |
| env: | |
| - name: DEMO_ENVIRONMENT | |
| value: "${{ inputs.environment }}" | |
| - name: POSTGRES_HOST | |
| value: "${POSTGRES_HOST}" | |
| - name: POSTGRES_USER | |
| value: "${POSTGRES_USER}" | |
| - name: POSTGRES_PASSWORD | |
| value: "${POSTGRES_PASSWORD}" | |
| - name: POSTGRES_DATABASE | |
| value: "${POSTGRES_DATABASE}" | |
| - name: POSTGRES_PORT | |
| value: "${POSTGRES_PORT}" | |
| - name: POSTGRES_SSL | |
| value: "${POSTGRES_SSL}" | |
| EOF | |
| kubectl wait --for=condition=complete "job/${JOB_NAME}" -n holiday-peak --timeout=10m | |
| kubectl logs "job/${JOB_NAME}" -n holiday-peak |