Skip to content

deploy-azd-core (reusable) #46

deploy-azd-core (reusable)

deploy-azd-core (reusable) #46

Workflow file for this run

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