|
| 1 | +name: Deploy Environment |
| 2 | + |
| 3 | +on: |
| 4 | + workflow_call: |
| 5 | + inputs: |
| 6 | + env_name: |
| 7 | + required: true |
| 8 | + type: string |
| 9 | + description: Environment name (dev/staging/prod) |
| 10 | + tf_state_key: |
| 11 | + required: true |
| 12 | + type: string |
| 13 | + description: Terraform state key (e.g., dev.terraform.tfstate) |
| 14 | + codex_model: |
| 15 | + required: true |
| 16 | + type: string |
| 17 | + description: Codex model deployment name |
| 18 | + codex_api_version: |
| 19 | + required: true |
| 20 | + type: string |
| 21 | + description: Codex API version |
| 22 | + terraform_working_directory: |
| 23 | + required: true |
| 24 | + type: string |
| 25 | + description: Terraform working directory (e.g., infra/env/dev) |
| 26 | + smoke_retry_sleep: |
| 27 | + required: false |
| 28 | + type: string |
| 29 | + default: "10" |
| 30 | + description: Retry sleep for smoke tests |
| 31 | + smoke_models_wait_sleep: |
| 32 | + required: false |
| 33 | + type: string |
| 34 | + default: "15" |
| 35 | + description: Wait sleep for model registration |
| 36 | + include_aoai_host_check: |
| 37 | + required: false |
| 38 | + type: boolean |
| 39 | + default: false |
| 40 | + description: Include AOAI endpoint host validation |
| 41 | + secrets: |
| 42 | + AZURE_OPENAI_ENDPOINT: |
| 43 | + required: true |
| 44 | + AZURE_OPENAI_API_KEY: |
| 45 | + required: true |
| 46 | + AZURE_OPENAI_EMBEDDING_ENDPOINT: |
| 47 | + required: true |
| 48 | + AZURE_OPENAI_EMBEDDING_API_KEY: |
| 49 | + required: true |
| 50 | + AIGATEWAY_KEY: |
| 51 | + required: true |
| 52 | + |
| 53 | +env: |
| 54 | + TF_VAR_env: ${{ inputs.env_name }} |
| 55 | + TF_VAR_projname: "aigateway" |
| 56 | + TF_VAR_location: "southafricanorth" |
| 57 | + TF_VAR_location_short: "san" |
| 58 | + TF_VAR_azure_openai_endpoint: ${{ secrets.AZURE_OPENAI_ENDPOINT }} |
| 59 | + TF_VAR_azure_openai_api_key: ${{ secrets.AZURE_OPENAI_API_KEY }} |
| 60 | + TF_VAR_azure_openai_embedding_endpoint: ${{ secrets.AZURE_OPENAI_EMBEDDING_ENDPOINT }} |
| 61 | + TF_VAR_azure_openai_embedding_api_key: ${{ secrets.AZURE_OPENAI_EMBEDDING_API_KEY }} |
| 62 | + TF_VAR_gateway_key: ${{ secrets.AIGATEWAY_KEY }} |
| 63 | + TF_VAR_codex_model: ${{ inputs.codex_model }} |
| 64 | + TF_VAR_codex_api_version: ${{ inputs.codex_api_version }} |
| 65 | + TF_VAR_embedding_deployment: "text-embedding-3-large" |
| 66 | + TF_VAR_embeddings_api_version: "2024-02-01" |
| 67 | + |
| 68 | +jobs: |
| 69 | + deploy: |
| 70 | + runs-on: ubuntu-latest |
| 71 | + defaults: |
| 72 | + run: |
| 73 | + working-directory: ${{ inputs.terraform_working_directory }} |
| 74 | + |
| 75 | + steps: |
| 76 | + - name: Checkout code |
| 77 | + uses: actions/checkout@v4 |
| 78 | + |
| 79 | + - name: Quickcheck required secrets and config |
| 80 | + shell: bash |
| 81 | + run: | |
| 82 | + set -euo pipefail |
| 83 | + missing=0 |
| 84 | + required=( |
| 85 | + AZURE_CLIENT_ID |
| 86 | + AZURE_TENANT_ID |
| 87 | + AZURE_SUBSCRIPTION_ID |
| 88 | + TF_BACKEND_RG |
| 89 | + TF_BACKEND_SA |
| 90 | + TF_BACKEND_CONTAINER |
| 91 | + TF_VAR_azure_openai_endpoint |
| 92 | + TF_VAR_azure_openai_api_key |
| 93 | + TF_VAR_gateway_key |
| 94 | + ) |
| 95 | + for v in "${required[@]}"; do |
| 96 | + if [ -z "${!v:-}" ]; then |
| 97 | + echo "::error::Missing required value: ${v}" |
| 98 | + missing=1 |
| 99 | + else |
| 100 | + echo "${v}=SET" |
| 101 | + fi |
| 102 | + done |
| 103 | + echo "TF_VAR_env=${TF_VAR_env:-unset}" |
| 104 | + echo "TF_VAR_embedding_deployment=${TF_VAR_embedding_deployment:-unset}" |
| 105 | + echo "TF_VAR_codex_model=${TF_VAR_codex_model:-unset}" |
| 106 | + if [ -n "${TF_VAR_azure_openai_endpoint:-}" ]; then |
| 107 | + echo "Azure OpenAI endpoint=${TF_VAR_azure_openai_endpoint}" |
| 108 | + endpoint_host=$(echo "${TF_VAR_azure_openai_endpoint}" | sed -E 's#^https?://([^/]+)/?.*$#\1#') |
| 109 | + echo "Azure OpenAI endpoint host=${endpoint_host}" |
| 110 | + if [ "${{ inputs.include_aoai_host_check }}" = "true" ] && [ -n "${EXPECTED_AOAI_ENDPOINT_HOST:-}" ] && [ "${endpoint_host}" != "${EXPECTED_AOAI_ENDPOINT_HOST}" ]; then |
| 111 | + echo "::error::Prod AOAI endpoint host mismatch. Expected '${EXPECTED_AOAI_ENDPOINT_HOST}', got '${endpoint_host}'. Check environment secret AZURE_OPENAI_ENDPOINT." |
| 112 | + missing=1 |
| 113 | + fi |
| 114 | + fi |
| 115 | + if [ "${missing}" -ne 0 ]; then |
| 116 | + exit 1 |
| 117 | + fi |
| 118 | +
|
| 119 | + - name: Azure Login |
| 120 | + uses: azure/login@v2 |
| 121 | + with: |
| 122 | + client-id: ${{ env.AZURE_CLIENT_ID }} |
| 123 | + tenant-id: ${{ env.AZURE_TENANT_ID }} |
| 124 | + subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }} |
| 125 | + |
| 126 | + - name: Setup Terraform |
| 127 | + uses: hashicorp/setup-terraform@v3 |
| 128 | + with: |
| 129 | + terraform_version: 1.14.6 |
| 130 | + |
| 131 | + - name: Terraform Init |
| 132 | + run: | |
| 133 | + terraform init \ |
| 134 | + -backend-config="resource_group_name=${TF_BACKEND_RG}" \ |
| 135 | + -backend-config="storage_account_name=${TF_BACKEND_SA}" \ |
| 136 | + -backend-config="container_name=${TF_BACKEND_CONTAINER}" \ |
| 137 | + -backend-config="key=${{ inputs.tf_state_key }}" |
| 138 | +
|
| 139 | + - name: Import existing Container App into Terraform state |
| 140 | + uses: ./.github/actions/import-container-app |
| 141 | + with: |
| 142 | + projname: ${{ env.TF_VAR_projname }} |
| 143 | + env: ${{ env.TF_VAR_env }} |
| 144 | + location_short: ${{ env.TF_VAR_location_short }} |
| 145 | + subscription_id: ${{ env.AZURE_SUBSCRIPTION_ID }} |
| 146 | + terraform_working_directory: ${{ inputs.terraform_working_directory }} |
| 147 | + |
| 148 | + - name: Terraform Plan |
| 149 | + run: | |
| 150 | + terraform plan -out=tfplan |
| 151 | +
|
| 152 | + - name: Terraform Apply |
| 153 | + run: | |
| 154 | + terraform apply -auto-approve tfplan |
| 155 | +
|
| 156 | + - name: Get gateway URL |
| 157 | + id: gw |
| 158 | + run: echo "url=$(terraform output -raw gateway_url)" >> $GITHUB_OUTPUT |
| 159 | + |
| 160 | + - name: Get dashboard URL |
| 161 | + id: db |
| 162 | + run: echo "url=$(terraform output -raw dashboard_url 2>/dev/null || true)" >> $GITHUB_OUTPUT |
| 163 | + |
| 164 | + - name: Runtime diagnostics (Container App config) |
| 165 | + shell: bash |
| 166 | + run: | |
| 167 | + set -euo pipefail |
| 168 | + RG_NAME="pvc-${TF_VAR_env}-${TF_VAR_projname}-rg-${TF_VAR_location_short}" |
| 169 | + CA_NAME="pvc-${TF_VAR_env}-${TF_VAR_projname}-ca-${TF_VAR_location_short}" |
| 170 | + echo "Resource Group: ${RG_NAME}" |
| 171 | + echo "Container App: ${CA_NAME}" |
| 172 | + echo "Gateway URL (terraform output): ${{ steps.gw.outputs.url }}" |
| 173 | + echo "Latest revision:" |
| 174 | + az containerapp show -g "${RG_NAME}" -n "${CA_NAME}" --query "properties.latestRevisionName" -o tsv |
| 175 | + echo "Active revisions (name, active, created):" |
| 176 | + az containerapp revision list -g "${RG_NAME}" -n "${CA_NAME}" --query "[].{name:name,active:properties.active,created:properties.createdTime}" -o table |
| 177 | + echo "Configured env vars for LiteLLM secret refs:" |
| 178 | + az containerapp show -g "${RG_NAME}" -n "${CA_NAME}" --query "properties.template.containers[0].env[?name=='LITELLM_AZURE_OPENAI_API_KEY' || name=='LITELLM_GATEWAY_KEY']" -o json |
| 179 | + echo "Configured secret sources (names + key vault URLs):" |
| 180 | + az containerapp show -g "${RG_NAME}" -n "${CA_NAME}" --query "properties.configuration.secrets[].{name:name,keyVaultUrl:keyVaultUrl}" -o table |
| 181 | + echo "LITELLM_CONFIG_CONTENT excerpt (first 2000 chars):" |
| 182 | + az containerapp show -g "${RG_NAME}" -n "${CA_NAME}" --query "properties.template.containers[0].env[?name=='LITELLM_CONFIG_CONTENT'].value | [0]" -o tsv | head -c 2000 || true |
| 183 | + echo |
| 184 | +
|
| 185 | + - name: Integration test (Azure OpenAI backend) |
| 186 | + shell: bash |
| 187 | + env: |
| 188 | + AZURE_OPENAI_ENDPOINT: ${{ env.TF_VAR_azure_openai_endpoint }} |
| 189 | + AZURE_OPENAI_API_KEY: ${{ env.TF_VAR_azure_openai_api_key }} |
| 190 | + AZURE_OPENAI_EMBEDDING_ENDPOINT: ${{ env.TF_VAR_azure_openai_embedding_endpoint }} |
| 191 | + AZURE_OPENAI_EMBEDDING_API_KEY: ${{ env.TF_VAR_azure_openai_embedding_api_key }} |
| 192 | + AZURE_OPENAI_EMBEDDING_DEPLOYMENT: ${{ env.TF_VAR_embedding_deployment }} |
| 193 | + AZURE_OPENAI_API_VERSION: ${{ env.TF_VAR_embeddings_api_version }} |
| 194 | + AZURE_OPENAI_CHAT_DEPLOYMENT: "gpt-4.1" |
| 195 | + AZURE_OPENAI_CHAT_API_VERSION: ${{ env.TF_VAR_codex_api_version }} |
| 196 | + AZURE_OPENAI_CODEX_MODEL: ${{ env.TF_VAR_codex_model }} |
| 197 | + working-directory: ${{ github.workspace }} |
| 198 | + run: python3 scripts/integration_test.py |
| 199 | + |
| 200 | + - name: Smoke test gateway (embeddings + responses) |
| 201 | + uses: ./.github/actions/smoke-test-gateway |
| 202 | + with: |
| 203 | + gateway_url: ${{ steps.gw.outputs.url }} |
| 204 | + gateway_key: ${{ secrets.AIGATEWAY_KEY }} |
| 205 | + embedding_model: ${{ env.TF_VAR_embedding_deployment }} |
| 206 | + codex_model: ${{ env.TF_VAR_codex_model }} |
| 207 | + aoai_endpoint: ${{ env.TF_VAR_azure_openai_endpoint }} |
| 208 | + aoai_api_key: ${{ env.TF_VAR_azure_openai_api_key }} |
| 209 | + max_attempts: "3" |
| 210 | + retry_sleep: ${{ inputs.smoke_retry_sleep }} |
| 211 | + models_wait_attempts: ${{ if(inputs.env_name == 'prod', '3', '1') }} |
| 212 | + models_wait_sleep: ${{ inputs.smoke_models_wait_sleep }} |
| 213 | + |
| 214 | + - name: Smoke test shared state API (dashboard proxy) |
| 215 | + if: env.TF_VAR_state_service_container_image != '' |
| 216 | + shell: bash |
| 217 | + run: | |
| 218 | + set -euo pipefail |
| 219 | + DASHBOARD_URL="${{ steps.db.outputs.url }}" |
| 220 | + TEST_USER="ci-smoke-${TF_VAR_env}" |
| 221 | +
|
| 222 | + curl -fsS --connect-timeout 5 --max-time 15 "${DASHBOARD_URL}/api/state/catalog" > /tmp/catalog.json |
| 223 | +
|
| 224 | + curl -fsS --connect-timeout 5 --max-time 15 -X PUT "${DASHBOARD_URL}/api/state/selection" \ |
| 225 | + -H "Content-Type: application/json" \ |
| 226 | + -H "X-User-Id: ${TEST_USER}" \ |
| 227 | + -d '{"enabled":true,"selected_model":"'"${TF_VAR_codex_model}"'"}' > /tmp/selection-put.json |
| 228 | +
|
| 229 | + curl -fsS --connect-timeout 5 --max-time 15 "${DASHBOARD_URL}/api/state/selection" \ |
| 230 | + -H "X-User-Id: ${TEST_USER}" > /tmp/selection-get.json |
| 231 | +
|
| 232 | + jq -e '.enabled == true' /tmp/selection-get.json > /dev/null |
0 commit comments