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
1 change: 1 addition & 0 deletions .github/workflows/agent-evaluation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ jobs:
agent-evaluation:
name: Agent Quality Evaluation
runs-on: ubuntu-latest
environment: ${{ inputs.environment || 'integration' }}
permissions:
contents: read
id-token: write # Needed for OIDC → DefaultAzureCredential
Expand Down
9 changes: 5 additions & 4 deletions .github/workflows/destroy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
terraform_destroy:
name: Terraform Destroy
runs-on: ubuntu-latest
# environment: ${{ inputs.environment || 'dev' }} # Commented out to use repo-level variables
environment: ${{ inputs.environment || 'integration' }}
permissions:
id-token: write
contents: read
Expand Down Expand Up @@ -66,13 +66,14 @@ jobs:
-var subscription_id=${{ vars.AZURE_SUBSCRIPTION_ID }} \
-var acr_name=${{ vars.ACR_NAME }} \
-var location=${{ vars.AZ_REGION }} \
-var environment=${{ inputs.environment || 'dev' }} \
-var environment=${{ inputs.environment || 'integration' }} \
-var docker_image_mcp=${{ vars.DOCKER_IMAGE_MCP }} \
-var docker_image_backend=${{ vars.DOCKER_IMAGE_BACKEND }} \
-var iteration=${{ inputs.environment || 'dev' }}
-var iteration=${{ vars.ITERATION }}
env:
TFSTATE_RG: ${{ vars.TFSTATE_RG }}
TFSTATE_ACCOUNT: ${{ vars.TFSTATE_ACCOUNT }}
TFSTATE_CONTAINER: ${{ vars.TFSTATE_CONTAINER }}
TFSTATE_KEY: "${{ github.event.repository.name }}-${{ github.ref_name }}.tfstate"
# Use environment name for state key — must match infrastructure.yml
TFSTATE_KEY: "${{ github.event.repository.name }}-${{ inputs.environment || 'integration' }}.tfstate"

5 changes: 3 additions & 2 deletions .github/workflows/docker-application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
build:
name: Build & Push Backend Image
runs-on: ubuntu-latest
# environment: ${{ inputs.environment || 'dev' }} # Commented out to use repo-level variables
environment: ${{ inputs.environment || 'integration' }}
permissions:
id-token: write
contents: read
Expand All @@ -46,10 +46,11 @@ jobs:
id: acr
run: |
# Construct ACR name matching Terraform pattern: {project}{env}acr{iteration}
# ACR names must be alphanumeric — strip hyphens to match Terraform's replace("-", "")
PROJECT="${{ vars.PROJECT_NAME || 'OpenAIWorkshop' }}"
ENV="${{ inputs.environment || 'dev' }}"
ITERATION="${{ vars.ITERATION || '002' }}"
ACR_NAME="${PROJECT}${ENV}acr${ITERATION}"
ACR_NAME=$(echo "${PROJECT}${ENV}acr${ITERATION}" | tr -d '-')
echo "name=${ACR_NAME}" >> $GITHUB_OUTPUT
echo "server=${ACR_NAME}.azurecr.io" >> $GITHUB_OUTPUT
echo "Using ACR: ${ACR_NAME}"
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/docker-mcp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
build:
name: Build & Push MCP Image
runs-on: ubuntu-latest
# environment: ${{ inputs.environment || 'dev' }} # Commented out to use repo-level variables
environment: ${{ inputs.environment || 'integration' }}
permissions:
id-token: write
contents: read
Expand All @@ -46,10 +46,11 @@ jobs:
id: acr
run: |
# Construct ACR name matching Terraform pattern: {project}{env}acr{iteration}
# ACR names must be alphanumeric — strip hyphens to match Terraform's replace("-", "")
PROJECT="${{ vars.PROJECT_NAME || 'OpenAIWorkshop' }}"
ENV="${{ inputs.environment || 'dev' }}"
ITERATION="${{ vars.ITERATION || '002' }}"
ACR_NAME="${PROJECT}${ENV}acr${ITERATION}"
ACR_NAME=$(echo "${PROJECT}${ENV}acr${ITERATION}" | tr -d '-')
echo "name=${ACR_NAME}" >> $GITHUB_OUTPUT
echo "server=${ACR_NAME}.azurecr.io" >> $GITHUB_OUTPUT
echo "Using ACR: ${ACR_NAME}"
Expand Down
92 changes: 72 additions & 20 deletions .github/workflows/infrastructure.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
tf:
name: Terraform Deployment
runs-on: ubuntu-latest
# environment: removed to use repo-level variables
environment: ${{ inputs.environment }}
if: ${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.iac-tool || 'tf') == 'tf' }}
permissions:
id-token: write
Expand All @@ -65,13 +65,13 @@ jobs:
- name: Terraform Setup
uses: hashicorp/setup-terraform@v3

- name: Sanitize branch name for state key
- name: Sanitize environment name for state key
id: sanitize
run: |
# Replace / and other invalid chars with - for valid Azure blob name
BRANCH="${{ github.head_ref || github.ref_name }}"
SAFE_BRANCH=$(echo "$BRANCH" | sed 's/[^a-zA-Z0-9._-]/-/g')
echo "branch=$SAFE_BRANCH" >> $GITHUB_OUTPUT
ENV="${{ inputs.environment }}"
SAFE_ENV=$(echo "$ENV" | sed 's/[^a-zA-Z0-9._-]/-/g')
echo "env=$SAFE_ENV" >> $GITHUB_OUTPUT

- name: Terraform Init/Plan/Apply
id: terraform
Expand All @@ -82,21 +82,73 @@ jobs:
export ARM_TENANT_ID="${{ vars.AZURE_TENANT_ID }}"
export ARM_SUBSCRIPTION_ID="${{ vars.AZURE_SUBSCRIPTION_ID }}"

# Common -var flags used by plan and import
TF_VARS=(
-var project_name=${{ github.event.repository.name }}
-var environment=${{ inputs.environment }}
-var tenant_id=${{ vars.AZURE_TENANT_ID }}
-var subscription_id=${{ vars.AZURE_SUBSCRIPTION_ID }}
-var acr_name=${{ vars.ACR_NAME }}
-var location=${{ vars.AZ_REGION }}
-var docker_image_mcp=${{ vars.DOCKER_IMAGE_MCP }}
-var docker_image_backend=${{ vars.DOCKER_IMAGE_BACKEND }}
-var iteration=${{ vars.ITERATION }}
)

terraform init -backend-config="resource_group_name=${TFSTATE_RG}" \
-backend-config="key=${TFSTATE_KEY}" -backend-config="storage_account_name=${TFSTATE_ACCOUNT}" \
-backend-config="container_name=${TFSTATE_CONTAINER}" -backend-config="use_oidc=true" -backend-config="use_azuread_auth=true"
terraform plan -out tfplan \
-var project_name=${{ github.event.repository.name }} \
-var environment=${{ github.event_name == 'workflow_dispatch' && github.event.inputs.environment || (github.base_ref == 'main' && 'prod') || (github.base_ref == 'int-agentic' && 'integration') || 'dev' }} \
-var tenant_id=${{ vars.AZURE_TENANT_ID }} \
-var subscription_id=${{ vars.AZURE_SUBSCRIPTION_ID }} \
-var acr_name=${{ vars.ACR_NAME }} \
-var location=${{ vars.AZ_REGION }} \
-var docker_image_mcp=${{ vars.DOCKER_IMAGE_MCP }} \
-var docker_image_backend=${{ vars.DOCKER_IMAGE_BACKEND }} \
-var iteration=${{ (github.event_name != 'workflow_dispatch' && github.base_ref != 'main' && github.base_ref != 'int-agentic') && '${GITHUB_SHA:0:7}' || vars.ITERATION }}

terraform apply -auto-approve tfplan

# ── Apply with auto-import on "already exists" errors ──
# If a prior run partially created resources but crashed before recording
# them in state, Terraform will fail with "already exists". This loop
# detects those errors, auto-imports the orphaned resources, and retries.
MAX_ATTEMPTS=3
for attempt in $(seq 1 $MAX_ATTEMPTS); do
echo "🔄 Terraform apply attempt $attempt/$MAX_ATTEMPTS"

terraform plan -out tfplan "${TF_VARS[@]}"

if terraform apply -auto-approve tfplan 2>&1 | tee /tmp/tf_apply.log; then
echo "✅ Terraform apply succeeded"
break
fi

# Check if the failure is due to "already exists" errors
if ! grep -q "already exists" /tmp/tf_apply.log; then
echo "❌ Terraform failed with a non-import error"
cat /tmp/tf_apply.log
exit 1
fi

if [ "$attempt" -eq "$MAX_ATTEMPTS" ]; then
echo "❌ Terraform failed after $MAX_ATTEMPTS attempts"
cat /tmp/tf_apply.log
exit 1
fi

echo "⚠️ Detected 'already exists' errors — auto-importing orphaned resources..."

# Parse error output: extract terraform address and Azure resource ID pairs
# Error format: with azurerm_container_app.mcp,
# followed by: a resource with the ID "/.../containerApps/ca-mcp-002" already exists
while IFS= read -r line; do
# Extract the TF resource address (e.g. azurerm_container_app.mcp)
tf_addr=$(echo "$line" | grep -oP 'with \K[a-zA-Z0-9_.]+(?=,)')
# Extract the Azure resource ID
azure_id=$(echo "$line" | grep -oP 'the ID "\K[^"]+')

if [ -n "$tf_addr" ] && [ -n "$azure_id" ]; then
echo " 📥 Importing $tf_addr → $azure_id"
terraform import "${TF_VARS[@]}" "$tf_addr" "$azure_id" || true
fi
done < <(
# Combine consecutive lines so address + ID are on the same logical line
cat /tmp/tf_apply.log | tr '\n' '§' | sed 's/§│/│/g' | tr '§' '\n' | grep "already exists"
)

echo "🔁 Retrying terraform apply..."
done

output=$(terraform output -raw openai_endpoint 2>/dev/null || true)
echo "MODEL_ENDPOINT=$output" >> $GITHUB_OUTPUT
Expand All @@ -109,12 +161,12 @@ jobs:
TFSTATE_RG: ${{ vars.TFSTATE_RG }}
TFSTATE_ACCOUNT: ${{ vars.TFSTATE_ACCOUNT }}
TFSTATE_CONTAINER: ${{ vars.TFSTATE_CONTAINER }}
# Use sanitized branch name for valid Azure blob name
TFSTATE_KEY: "${{ github.event.repository.name }}-${{ steps.sanitize.outputs.branch }}.tfstate"
# Use environment name for state key — each env gets its own TF state
TFSTATE_KEY: "${{ github.event.repository.name }}-${{ steps.sanitize.outputs.env }}.tfstate"

bicep:
runs-on: ubuntu-latest
# environment: removed to use repo-level variables
environment: ${{ inputs.environment }}
if: ${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.iac-tool || 'tf') == 'bicep' }}
permissions:
id-token: write
Expand Down
59 changes: 30 additions & 29 deletions .github/workflows/orchestrate.yml
Original file line number Diff line number Diff line change
@@ -1,31 +1,34 @@
name: Orchestrate Deployment
name: CI/CD Pipeline

# ─────────────────────────────────────────────────────────────────────
# Pipeline modes:
# PR → main / int-agentic ➜ tests-only (validate against existing env)
# Push → main ➜ full deploy (deploy to prod after merge)
# Push → tjs-infra-as-code ➜ full deploy (dev, with auto-destroy)
# Push → main ➜ full deploy (deploy to production)
# Push → *-dev ➜ full deploy (deploy to integration-<name>)
# Manual dispatch ➜ full deploy (chosen environment)
#
# Per-developer environments:
# Each developer pushes to their own <name>-dev branch.
# The pipeline maps <name>-dev → integration-<name> environment,
# which contains that developer's own Azure subscription credentials.
# ─────────────────────────────────────────────────────────────────────

on:
workflow_dispatch:
inputs:
target_env:
type: choice
description: Environment to deploy
options: [dev, test, prod]
type: string
description: "Environment to deploy (e.g. integration-james, production)"
required: true

pull_request:
branches:
- main
- int-agentic

push:
branches:
- main
- tjs-infra-as-code
- '*-dev'

permissions:
contents: read
Expand All @@ -51,19 +54,25 @@ jobs:
if [ "$EVENT" = "workflow_dispatch" ]; then
ENV="${{ inputs.target_env }}"
elif [ "$EVENT" = "pull_request" ]; then
# PRs: resolve from the target (base) branch
case "${{ github.base_ref }}" in
main) ENV="prod" ;;
int-agentic) ENV="integration" ;;
*) ENV="dev" ;;
main) ENV="production" ;;
*) ENV="integration" ;;
esac
elif [ "$EVENT" = "push" ]; then
case "${{ github.ref_name }}" in
main) ENV="prod" ;;
tjs-infra-as-code) ENV="dev" ;;
*) ENV="dev" ;;
BRANCH="${{ github.ref_name }}"
case "$BRANCH" in
main)
ENV="production" ;;
*-dev)
# Extract developer name: james-dev → integration-james
DEV_NAME="${BRANCH%-dev}"
ENV="integration-${DEV_NAME}" ;;
*)
ENV="integration" ;;
esac
else
ENV="dev"
ENV="integration"
fi

# ── Resolve pipeline mode ──
Expand All @@ -90,6 +99,7 @@ jobs:
needs: pipeline-config
if: needs.pipeline-config.outputs.full_deploy == 'true'
runs-on: ubuntu-latest
environment: ${{ needs.pipeline-config.outputs.environment }}
steps:
- name: Azure OIDC Login
uses: azure/login@v2
Expand Down Expand Up @@ -155,6 +165,7 @@ jobs:
needs: pipeline-config
if: needs.pipeline-config.outputs.full_deploy == 'false'
runs-on: ubuntu-latest
environment: ${{ needs.pipeline-config.outputs.environment }}
outputs:
backend_endpoint: ${{ steps.lookup.outputs.backend_endpoint }}
mcp_endpoint: ${{ steps.lookup.outputs.mcp_endpoint }}
Expand Down Expand Up @@ -242,17 +253,7 @@ jobs:
secrets: inherit

# ────────────────────────────────────────────────────────────────────
# Optional: Destroy infrastructure (dev branches only, after tests pass)
# NOTE: Auto-destroy is disabled. All environments (integration-* and
# production) persist their infrastructure. To tear down an environment
# manually, use: workflow_dispatch → destroy.yml with the target env.
# ────────────────────────────────────────────────────────────────────
destroy-infrastructure:
needs: [pipeline-config, integration-tests, agent-evaluation]
if: >-
always()
&& needs.pipeline-config.outputs.full_deploy == 'true'
&& needs.integration-tests.result == 'success'
&& (needs.agent-evaluation.result == 'success' || needs.agent-evaluation.result == 'skipped' || needs.agent-evaluation.result == 'failure')
&& (github.ref_name == 'tjs-infra-as-code' || github.ref_name == 'james-dev' || (inputs.target_env && inputs.target_env == 'dev'))
uses: ./.github/workflows/destroy.yml
with:
environment: ${{ needs.pipeline-config.outputs.environment }}
secrets: inherit
5 changes: 3 additions & 2 deletions .github/workflows/update-containers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
update-containers:
name: Update Container Apps
runs-on: ubuntu-latest
# environment: ${{ inputs.environment }} # Commented out to use repo-level variables
environment: ${{ inputs.environment }}
permissions:
id-token: write
contents: read
Expand Down Expand Up @@ -64,7 +64,8 @@ jobs:
echo "backend_app=ca-be-${ITERATION}" >> $GITHUB_OUTPUT

# ACR name follows Terraform pattern: {project}{env}acr{iteration}
ACR_NAME="${PROJECT}${ENV}acr${ITERATION}"
# ACR names must be alphanumeric — strip hyphens to match Terraform's replace("-", "")
ACR_NAME=$(echo "${PROJECT}${ENV}acr${ITERATION}" | tr -d '-')
echo "acr_name=${ACR_NAME}" >> $GITHUB_OUTPUT
echo "acr_server=${ACR_NAME}.azurecr.io" >> $GITHUB_OUTPUT
echo "Using ACR: ${ACR_NAME}"
Expand Down
Loading