Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/actions/import-container-app/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ inputs:
description: Project name component of the Container App name (TF_VAR_projname)
env:
required: true
description: Environment name (dev|uat|prod)
description: Environment name (dev|staging|prod)
location_short:
required: true
description: Short location code (TF_VAR_location_short)
Expand Down
6 changes: 3 additions & 3 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@
- [ ] No environment/config changes required
- [ ] Environment/config changes required (describe below)

## UAT Toggle (PRs to `main`)
## Staging Toggle (PRs to `main`)

- Add label `run-uat` to this PR to enable UAT deployment (`deploy-uat`).
- Remove label `run-uat` to skip UAT deployment.
- Add label `run-staging` to this PR to enable staging deployment (`deploy-staging`).
- Remove label `run-staging` to skip staging deployment.

## Risk / Rollback

Expand Down
232 changes: 232 additions & 0 deletions .github/workflows/deploy-environment.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
name: Deploy Environment

on:
workflow_call:
inputs:
env_name:
required: true
type: string
description: Environment name (dev/staging/prod)
tf_state_key:
required: true
type: string
description: Terraform state key (e.g., dev.terraform.tfstate)
codex_model:
required: true
type: string
description: Codex model deployment name
codex_api_version:
required: true
type: string
description: Codex API version
terraform_working_directory:
required: true
type: string
description: Terraform working directory (e.g., infra/env/dev)
smoke_retry_sleep:
required: false
type: string
default: "10"
description: Retry sleep for smoke tests
smoke_models_wait_sleep:
required: false
type: string
default: "15"
description: Wait sleep for model registration
include_aoai_host_check:
required: false
type: boolean
default: false
description: Include AOAI endpoint host validation
secrets:
AZURE_OPENAI_ENDPOINT:
required: true
AZURE_OPENAI_API_KEY:
required: true
AZURE_OPENAI_EMBEDDING_ENDPOINT:
required: true
AZURE_OPENAI_EMBEDDING_API_KEY:
required: true
AIGATEWAY_KEY:
required: true

env:
TF_VAR_env: ${{ inputs.env_name }}
TF_VAR_projname: "aigateway"
TF_VAR_location: "southafricanorth"
TF_VAR_location_short: "san"
TF_VAR_azure_openai_endpoint: ${{ secrets.AZURE_OPENAI_ENDPOINT }}
TF_VAR_azure_openai_api_key: ${{ secrets.AZURE_OPENAI_API_KEY }}
TF_VAR_azure_openai_embedding_endpoint: ${{ secrets.AZURE_OPENAI_EMBEDDING_ENDPOINT }}
TF_VAR_azure_openai_embedding_api_key: ${{ secrets.AZURE_OPENAI_EMBEDDING_API_KEY }}
TF_VAR_gateway_key: ${{ secrets.AIGATEWAY_KEY }}
TF_VAR_codex_model: ${{ inputs.codex_model }}
TF_VAR_codex_api_version: ${{ inputs.codex_api_version }}
TF_VAR_embedding_deployment: "text-embedding-3-large"
TF_VAR_embeddings_api_version: "2024-02-01"
Comment on lines +53 to +66
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Do reusable GitHub Actions workflows inherit the caller workflow's top-level env: values, or must those values be passed explicitly and rebound inside the called workflow?

💡 Result:

No—reusable (called) workflows do not inherit the caller workflow’s top-level env: values. GitHub explicitly states that “Any environment variables set in an env context defined at the workflow level in the caller workflow are not propagated to the called workflow.” [1]

So if the called workflow needs those values, you must pass them explicitly (typically via on: workflow_call: inputs: / jobs.<job_id>.with:) and then rebind/use them inside the called workflow. [1]


🏁 Script executed:

sed -n '211p' .github/workflows/deploy-environment.yaml

Repository: phoenixvc/ai-gateway

Length of output: 142


Rebuild the full deploy env inside the reusable workflow and fix Line 211 syntax.

Reusable workflows do not inherit the caller's top-level env: values—they must be passed explicitly. This workflow only recreates the AOAI/gateway subset, leaving AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID, TF_BACKEND_*, and other Terraform variables empty for Lines 79-137. The result is that apply executes with a different input set than the reviewed plan, TF_VAR_state_service_container_image stays empty so the Line 215 smoke gate never runs, and the prod AOAI guard at Lines 106-113 is inert because EXPECTED_AOAI_ENDPOINT_HOST is never exported. Additionally, Line 211 uses invalid syntax if(inputs.env_name == 'prod', '3', '1') — GitHub Actions has no if() function; use a ternary pattern instead: inputs.env_name == 'prod' && '3' || '1'.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/deploy-environment.yaml around lines 53 - 66, The reusable
workflow currently only sets a subset of env vars (TF_VAR_azure_openai_*,
TF_VAR_gateway_key, TF_VAR_codex_*, etc.), causing caller-level
secrets/TF_BACKEND_*/AZURE_CLIENT_ID/AZURE_TENANT_ID/AZURE_SUBSCRIPTION_ID,
EXPECTED_AOAI_ENDPOINT_HOST and TF_VAR_state_service_container_image to be empty
at apply time and breaking guards; update the env block to explicitly pass all
required variables from inputs/secrets (including AZURE_CLIENT_ID,
AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID, TF_BACKEND_*,
TF_VAR_state_service_container_image, EXPECTED_AOAI_ENDPOINT_HOST and any other
Terraform TF_VAR_* the plan relies on) so plan and apply see the same inputs,
and replace the invalid GitHub Actions function syntax if(inputs.env_name ==
'prod', '3', '1') with the supported ternary-like expression inputs.env_name ==
'prod' && '3' || '1' to restore the Line 211 value evaluation and ensure the
prod AOAI guard and Line 215 smoke gate run as intended.


jobs:
deploy:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ${{ inputs.terraform_working_directory }}

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Quickcheck required secrets and config
shell: bash
run: |
set -euo pipefail
missing=0
required=(
AZURE_CLIENT_ID
AZURE_TENANT_ID
AZURE_SUBSCRIPTION_ID
TF_BACKEND_RG
TF_BACKEND_SA
TF_BACKEND_CONTAINER
TF_VAR_azure_openai_endpoint
TF_VAR_azure_openai_api_key
TF_VAR_gateway_key
)
for v in "${required[@]}"; do
if [ -z "${!v:-}" ]; then
echo "::error::Missing required value: ${v}"
missing=1
else
echo "${v}=SET"
fi
done
echo "TF_VAR_env=${TF_VAR_env:-unset}"
echo "TF_VAR_embedding_deployment=${TF_VAR_embedding_deployment:-unset}"
echo "TF_VAR_codex_model=${TF_VAR_codex_model:-unset}"
if [ -n "${TF_VAR_azure_openai_endpoint:-}" ]; then
echo "Azure OpenAI endpoint=${TF_VAR_azure_openai_endpoint}"
endpoint_host=$(echo "${TF_VAR_azure_openai_endpoint}" | sed -E 's#^https?://([^/]+)/?.*$#\1#')
echo "Azure OpenAI endpoint host=${endpoint_host}"
if [ "${{ inputs.include_aoai_host_check }}" = "true" ] && [ -n "${EXPECTED_AOAI_ENDPOINT_HOST:-}" ] && [ "${endpoint_host}" != "${EXPECTED_AOAI_ENDPOINT_HOST}" ]; then
echo "::error::Prod AOAI endpoint host mismatch. Expected '${EXPECTED_AOAI_ENDPOINT_HOST}', got '${endpoint_host}'. Check environment secret AZURE_OPENAI_ENDPOINT."
missing=1
fi
fi
if [ "${missing}" -ne 0 ]; then
exit 1
fi

- name: Azure Login
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 Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.14.6

- name: Terraform Init
run: |
terraform init \
-backend-config="resource_group_name=${TF_BACKEND_RG}" \
-backend-config="storage_account_name=${TF_BACKEND_SA}" \
-backend-config="container_name=${TF_BACKEND_CONTAINER}" \
-backend-config="key=${{ inputs.tf_state_key }}"

- name: Import existing Container App into Terraform state
uses: ./.github/actions/import-container-app
with:
projname: ${{ env.TF_VAR_projname }}
env: ${{ env.TF_VAR_env }}
location_short: ${{ env.TF_VAR_location_short }}
subscription_id: ${{ env.AZURE_SUBSCRIPTION_ID }}
terraform_working_directory: ${{ inputs.terraform_working_directory }}

- name: Terraform Plan
run: |
terraform plan -out=tfplan

- name: Terraform Apply
run: |
terraform apply -auto-approve tfplan

- name: Get gateway URL
id: gw
run: echo "url=$(terraform output -raw gateway_url)" >> $GITHUB_OUTPUT

- name: Get dashboard URL
id: db
run: echo "url=$(terraform output -raw dashboard_url 2>/dev/null || true)" >> $GITHUB_OUTPUT

- name: Runtime diagnostics (Container App config)
shell: bash
run: |
set -euo pipefail
RG_NAME="pvc-${TF_VAR_env}-${TF_VAR_projname}-rg-${TF_VAR_location_short}"
CA_NAME="pvc-${TF_VAR_env}-${TF_VAR_projname}-ca-${TF_VAR_location_short}"
echo "Resource Group: ${RG_NAME}"
echo "Container App: ${CA_NAME}"
echo "Gateway URL (terraform output): ${{ steps.gw.outputs.url }}"
echo "Latest revision:"
az containerapp show -g "${RG_NAME}" -n "${CA_NAME}" --query "properties.latestRevisionName" -o tsv
echo "Active revisions (name, active, created):"
az containerapp revision list -g "${RG_NAME}" -n "${CA_NAME}" --query "[].{name:name,active:properties.active,created:properties.createdTime}" -o table
echo "Configured env vars for LiteLLM secret refs:"
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
echo "Configured secret sources (names + key vault URLs):"
az containerapp show -g "${RG_NAME}" -n "${CA_NAME}" --query "properties.configuration.secrets[].{name:name,keyVaultUrl:keyVaultUrl}" -o table
echo "LITELLM_CONFIG_CONTENT excerpt (first 2000 chars):"
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
echo

- name: Integration test (Azure OpenAI backend)
shell: bash
env:
AZURE_OPENAI_ENDPOINT: ${{ env.TF_VAR_azure_openai_endpoint }}
AZURE_OPENAI_API_KEY: ${{ env.TF_VAR_azure_openai_api_key }}
AZURE_OPENAI_EMBEDDING_ENDPOINT: ${{ env.TF_VAR_azure_openai_embedding_endpoint }}
AZURE_OPENAI_EMBEDDING_API_KEY: ${{ env.TF_VAR_azure_openai_embedding_api_key }}
AZURE_OPENAI_EMBEDDING_DEPLOYMENT: ${{ env.TF_VAR_embedding_deployment }}
AZURE_OPENAI_API_VERSION: ${{ env.TF_VAR_embeddings_api_version }}
AZURE_OPENAI_CHAT_DEPLOYMENT: "gpt-4.1"
AZURE_OPENAI_CHAT_API_VERSION: ${{ env.TF_VAR_codex_api_version }}
AZURE_OPENAI_CODEX_MODEL: ${{ env.TF_VAR_codex_model }}
working-directory: ${{ github.workspace }}
run: python3 scripts/integration_test.py

- name: Smoke test gateway (embeddings + responses)
uses: ./.github/actions/smoke-test-gateway
with:
gateway_url: ${{ steps.gw.outputs.url }}
gateway_key: ${{ secrets.AIGATEWAY_KEY }}
embedding_model: ${{ env.TF_VAR_embedding_deployment }}
codex_model: ${{ env.TF_VAR_codex_model }}
aoai_endpoint: ${{ env.TF_VAR_azure_openai_endpoint }}
aoai_api_key: ${{ env.TF_VAR_azure_openai_api_key }}
max_attempts: "3"
retry_sleep: ${{ inputs.smoke_retry_sleep }}
models_wait_attempts: ${{ if(inputs.env_name == 'prod', '3', '1') }}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Does GitHub Actions expression syntax support if()inside${{ }}expressions, or should ternary behavior be expressed with&&/||?

💡 Result:

GitHub Actions expressions do not provide an if() function you can call inside ${{ ... }}. ${{ ... }} supports literals, operators (including && / ||), and a fixed set of built-in functions—if() is not one of them. [1]

So for “ternary-like” value selection you have two options:

  • Recommended (now supported): use the case() expression function for real if/else behavior, e.g.
    ${{ case(condition, 'a', 'b') }}. [2][3]
  • Older workaround: use condition && valueIfTrue || valueIfFalse (note this can misbehave if valueIfTrue is falsy like '', 0, or false, because || will then fall through). && / || are the supported logical operators. [1]

Sources:
[1] GitHub Docs – Operators in expressions (&&, ||)
[2] GitHub Docs – case() function reference and examples
[3] GitHub Changelog (Jan 29, 2026) – announcement of new case function


Replace the unsupported if() expression with the case() function.

GitHub Actions expressions do not support if() as a function inside ${{ }}. Use the case() expression function for conditional value selection:

Suggested fix
-          models_wait_attempts: ${{ if(inputs.env_name == 'prod', '3', '1') }}
+          models_wait_attempts: ${{ case(inputs.env_name == 'prod', '3', '1') }}

Alternatively, the condition && value1 || value2 pattern works for non-falsy values but has pitfalls if value1 is falsy.

🧰 Tools
🪛 actionlint (1.7.11)

[error] 211-211: undefined function "if". available functions are "always", "cancelled", "case", "contains", "endswith", "failure", "format", "fromjson", "hashfiles", "join", "startswith", "success", "tojson"

(expression)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/deploy-environment.yaml at line 211, The
models_wait_attempts assignment uses the unsupported if() expression; update the
expression for models_wait_attempts to use GitHub Actions' case() function to
select '3' when inputs.env_name == 'prod' otherwise '1' (reference the
models_wait_attempts key in the deploy-environment workflow); ensure the case()
expression compares inputs.env_name to 'prod' and returns the correct string
values, or alternatively use the safe conditional pattern (condition && value1
|| value2) only if value1 cannot be falsy.

models_wait_sleep: ${{ inputs.smoke_models_wait_sleep }}

- name: Smoke test shared state API (dashboard proxy)
if: env.TF_VAR_state_service_container_image != ''
shell: bash
run: |
set -euo pipefail
DASHBOARD_URL="${{ steps.db.outputs.url }}"
TEST_USER="ci-smoke-${TF_VAR_env}"

curl -fsS --connect-timeout 5 --max-time 15 "${DASHBOARD_URL}/api/state/catalog" > /tmp/catalog.json

curl -fsS --connect-timeout 5 --max-time 15 -X PUT "${DASHBOARD_URL}/api/state/selection" \
-H "Content-Type: application/json" \
-H "X-User-Id: ${TEST_USER}" \
-d '{"enabled":true,"selected_model":"'"${TF_VAR_codex_model}"'"}' > /tmp/selection-put.json

curl -fsS --connect-timeout 5 --max-time 15 "${DASHBOARD_URL}/api/state/selection" \
-H "X-User-Id: ${TEST_USER}" > /tmp/selection-get.json

jq -e '.enabled == true' /tmp/selection-get.json > /dev/null
Loading