Skip to content

fix(db): widen purchase_history.account_id to VARCHAR(255) (#1677) #933

fix(db): widen purchase_history.account_id to VARCHAR(255) (#1677)

fix(db): widen purchase_history.account_id to VARCHAR(255) (#1677) #933

Workflow file for this run

# Deploy to Azure Container Apps
#
# This workflow deploys the CUDly application to Azure Container Apps.
# Serverless container platform with automatic scaling and built-in HTTPS.
#
# Required GitHub Secrets:
# - AZURE_CLIENT_ID: App registration client ID for OIDC keyless auth
# - AZURE_TENANT_ID: Azure AD tenant ID for OIDC keyless auth
# - AZURE_SUBSCRIPTION_ID: Azure subscription ID
# - ADMIN_EMAIL: Admin email for notifications
#
# Required GitHub Variables:
# - AZURE_LOCATION: Azure region (e.g., eastus)
# - ACR_NAME: Azure Container Registry name
# - RESOURCE_GROUP: Azure resource group name
# - KEY_VAULT_NAME: Azure Key Vault name
#
# Triggered by:
# - Pushes to main branch
# - Manual workflow dispatch
# - Workflow call from deploy-all.yml
name: Deploy to Azure Container Apps
permissions:
id-token: write
contents: read
concurrency:
group: deploy-azure-${{ github.ref }}
cancel-in-progress: false
on:
push:
branches: [main]
workflow_dispatch:
inputs:
environment:
description: 'Environment'
required: true
type: choice
options: [dev, staging, prod]
workflow_call:
inputs:
environment:
required: true
type: string
secrets:
ADMIN_EMAIL:
required: true
AZURE_CLIENT_ID:
required: true
AZURE_SUBSCRIPTION_ID:
required: true
AZURE_TENANT_ID:
required: true
TF_BACKEND_AZURE:
required: true
env:
AZURE_LOCATION: ${{ vars.AZURE_LOCATION || 'westus2' }}
ACR_NAME: ${{ vars.ACR_NAME || 'cudlyacr' }}
RESOURCE_GROUP: ${{ vars.RESOURCE_GROUP || 'cudly-rg' }}
TF_VERSION: '1.10.0'
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
# Determine deployment environment
prepare:
name: Prepare Deployment
runs-on: ubuntu-latest
outputs:
environment: ${{ steps.set-env.outputs.environment }}
steps:
- name: Determine environment
id: set-env
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
echo "environment=${{ inputs.environment }}" >> $GITHUB_OUTPUT
elif [[ "${{ github.event_name }}" == "workflow_call" ]]; then
echo "environment=${{ inputs.environment }}" >> $GITHUB_OUTPUT
else
echo "environment=dev" >> $GITHUB_OUTPUT
fi
# Build Docker image (via Terraform build module) and deploy
build-and-deploy:
name: Build & Deploy
runs-on: ubuntu-latest
needs: prepare
outputs:
app_url: ${{ steps.deploy.outputs.app_url }}
app_name: ${{ steps.deploy.outputs.app_name }}
env:
ARM_USE_OIDC: "true"
ARM_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
ARM_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
steps:
- name: Checkout code
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- name: Azure Login
uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Setup Terraform
uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Break stale state lock (if any)
env:
TF_BACKEND: ${{ secrets.TF_BACKEND_AZURE }}
ENVIRONMENT: ${{ needs.prepare.outputs.environment }}
run: |
printf '%s\nkey = "github-%s.terraform.tfstate"\n' "$TF_BACKEND" "$ENVIRONMENT" > /tmp/backend.tfbackend
STORAGE_ACCOUNT=$(grep -E '^\s*storage_account_name\s*=' /tmp/backend.tfbackend 2>/dev/null | tr -d ' "' | cut -d= -f2)
CONTAINER=$(grep -E '^\s*container_name\s*=' /tmp/backend.tfbackend 2>/dev/null | tr -d ' "' | cut -d= -f2)
RESOURCE_GROUP=$(grep -E '^\s*resource_group_name\s*=' /tmp/backend.tfbackend 2>/dev/null | tr -d ' "' | cut -d= -f2)
if [ -n "$STORAGE_ACCOUNT" ] && [ -n "$CONTAINER" ]; then
STATE_KEY="github-${ENVIRONMENT}.terraform.tfstate"
ACCOUNT_KEY=$(az storage account keys list \
--account-name "${STORAGE_ACCOUNT}" \
${RESOURCE_GROUP:+--resource-group "${RESOURCE_GROUP}"} \
--query '[0].value' -o tsv 2>/dev/null || echo "")
if [ -n "$ACCOUNT_KEY" ]; then
echo "Breaking any stale blob lease on ${STATE_KEY}..."
az storage blob lease break \
--account-name "${STORAGE_ACCOUNT}" \
--container-name "${CONTAINER}" \
--blob-name "${STATE_KEY}" \
--account-key "${ACCOUNT_KEY}" 2>/dev/null || echo "No lease to break (clean)"
fi
fi
- name: Terraform Init
run: |
cd terraform/environments/azure
terraform init -backend-config=/tmp/backend.tfbackend
- name: Terraform Plan
env:
TF_VAR_admin_email: ${{ secrets.ADMIN_EMAIL }}
TF_VAR_subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
run: |
cd terraform/environments/azure
terraform plan \
-var-file="github-${{ needs.prepare.outputs.environment }}.tfvars" \
-var="location=${{ env.AZURE_LOCATION }}" \
-out=tfplan
- name: Wait for resource group deletion to complete
run: |
ENVIRONMENT="${{ needs.prepare.outputs.environment }}"
RG_NAME="cudly-${ENVIRONMENT}-rg"
echo "Checking resource group ${RG_NAME} state..."
for i in $(seq 1 30); do
STATE=$(az group show --name "${RG_NAME}" --query "properties.provisioningState" -o tsv 2>/dev/null || echo "NotFound")
if [ "$STATE" = "Deleting" ]; then
echo " Resource group is being deleted ($i/30), waiting 30s..."
sleep 30
else
echo " Resource group state: ${STATE} — proceeding"
break
fi
if [ "$i" -eq 30 ]; then
echo "Timed out waiting for resource group deletion"
exit 1
fi
done
- name: Remove orphaned or soft-deleted Key Vault
run: |
ENVIRONMENT="${{ needs.prepare.outputs.environment }}"
KV_NAME="cudly-${ENVIRONMENT}-kv"
# If the KV exists in Azure but not in Terraform state (orphaned from a failed run),
# delete it so Terraform can re-create it cleanly
KV_RG=$(az keyvault show --name "${KV_NAME}" --query "resourceGroup" -o tsv 2>/dev/null || echo "")
if [ -n "$KV_RG" ]; then
echo "Key Vault ${KV_NAME} exists in Azure (rg: ${KV_RG}) — deleting so Terraform can recreate it"
az keyvault delete --name "${KV_NAME}" --resource-group "${KV_RG}" 2>/dev/null || true
fi
# Purge any soft-deleted Key Vault (including the one we just deleted)
# so the name can be immediately reused
DELETED=$(az keyvault list-deleted --query "[?name=='${KV_NAME}'].name" -o tsv 2>/dev/null || echo "")
if [ -n "$DELETED" ]; then
echo "Purging soft-deleted Key Vault ${KV_NAME}..."
az keyvault purge --name "${KV_NAME}" --location "${{ env.AZURE_LOCATION }}" 2>/dev/null \
&& echo "Purged" || echo "Purge failed (continuing)"
fi
- name: Terraform Apply
id: deploy
env:
TF_VAR_admin_email: ${{ secrets.ADMIN_EMAIL }}
TF_VAR_subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
run: |
cd terraform/environments/azure
terraform apply -auto-approve tfplan
# Get app URL
APP_URL=$(terraform output -raw container_app_url 2>/dev/null | grep -v '::' || echo "")
echo "app_url=$APP_URL" >> $GITHUB_OUTPUT
# Get app name
APP_NAME=$(terraform output -raw container_app_name 2>/dev/null | grep -v '::' || echo "")
echo "app_name=$APP_NAME" >> $GITHUB_OUTPUT
- name: Release state lock on failure
if: failure() || cancelled()
env:
ENVIRONMENT: ${{ needs.prepare.outputs.environment }}
run: |
STORAGE_ACCOUNT=$(grep -E '^\s*storage_account_name\s*=' /tmp/backend.tfbackend 2>/dev/null | tr -d ' "' | cut -d= -f2)
CONTAINER=$(grep -E '^\s*container_name\s*=' /tmp/backend.tfbackend 2>/dev/null | tr -d ' "' | cut -d= -f2)
RESOURCE_GROUP=$(grep -E '^\s*resource_group_name\s*=' /tmp/backend.tfbackend 2>/dev/null | tr -d ' "' | cut -d= -f2)
if [ -n "$STORAGE_ACCOUNT" ] && [ -n "$CONTAINER" ]; then
STATE_KEY="github-${ENVIRONMENT}.terraform.tfstate"
ACCOUNT_KEY=$(az storage account keys list \
--account-name "${STORAGE_ACCOUNT}" \
${RESOURCE_GROUP:+--resource-group "${RESOURCE_GROUP}"} \
--query '[0].value' -o tsv 2>/dev/null || echo "")
if [ -n "$ACCOUNT_KEY" ]; then
echo "Breaking Azure Blob lease on ${STATE_KEY}"
az storage blob lease break \
--account-name "${STORAGE_ACCOUNT}" \
--container-name "${CONTAINER}" \
--blob-name "${STATE_KEY}" \
--account-key "${ACCOUNT_KEY}" 2>/dev/null || echo "No lease to break (already clean)"
else
echo "WARNING: Could not get storage account key — lease may remain held"
fi
fi
- name: Save deployment info
run: |
cat <<EOF > deployment-info.json
{
"environment": "${{ needs.prepare.outputs.environment }}",
"app_url": "${{ steps.deploy.outputs.app_url }}",
"app_name": "${{ steps.deploy.outputs.app_name }}",
"deployed_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"deployed_by": "${{ github.actor }}",
"commit": "${{ github.sha }}"
}
EOF
cat deployment-info.json
- name: Upload deployment info
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: deployment-info-azure-${{ needs.prepare.outputs.environment }}
path: deployment-info.json
retention-days: 90
# Test the deployment
test-deployment:
name: Test Deployment
runs-on: ubuntu-latest
needs: [prepare, build-and-deploy]
if: always() && needs.build-and-deploy.result == 'success'
steps:
- name: Azure Login
uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Get Container App URL
id: get-url
run: |
APP_URL="${{ needs.build-and-deploy.outputs.app_url }}"
if [ -z "$APP_URL" ]; then
echo "Failed to get app URL from deploy outputs"
exit 1
fi
# Ensure URL has scheme and no trailing slash
case "$APP_URL" in
http://*|https://*) ;; # already has scheme
*) APP_URL="https://$APP_URL" ;;
esac
APP_URL="${APP_URL%/}"
echo "url=$APP_URL" >> $GITHUB_OUTPUT
- name: Wait for Container App to be ready
run: |
echo "Waiting 30 seconds for Container App to be ready..."
sleep 30
- name: Test health endpoint
run: |
URL="${{ steps.get-url.outputs.url }}"
echo "Testing health endpoint: $URL/health"
for i in {1..5}; do
if curl -f -s "$URL/health" > /dev/null; then
RESPONSE=$(curl -s "$URL/health")
echo "Response: $RESPONSE"
if echo "$RESPONSE" | grep -q '"status"'; then
echo "Health check passed!"
exit 0
fi
fi
echo "Attempt $i failed, retrying in 10 seconds..."
sleep 10
done
echo "Health check failed after 5 attempts"
exit 1
- name: Run smoke tests
run: |
URL="${{ steps.get-url.outputs.url }}"
echo "Running basic smoke tests..."
for i in {1..3}; do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$URL/health")
if [ "$STATUS" -eq 200 ]; then
echo "Health check $i: passed (HTTP $STATUS)"
else
echo "Health check $i: failed (HTTP $STATUS)"
exit 1
fi
sleep 2
done
echo "All smoke tests passed!"
- name: Check Container App logs
run: |
APP_NAME="${{ needs.build-and-deploy.outputs.app_name }}"
if [ -n "$APP_NAME" ]; then
echo "Recent Container App logs:"
az containerapp logs show \
--name $APP_NAME \
--resource-group ${{ env.RESOURCE_GROUP }} \
--tail 50 || true
fi
# Post deployment summary
summary:
name: Deployment Summary
runs-on: ubuntu-latest
needs: [prepare, build-and-deploy, test-deployment]
if: always()
steps:
- name: Download deployment info
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: deployment-info-azure-${{ needs.prepare.outputs.environment }}
continue-on-error: true
- name: Post summary
run: |
echo "## Azure Container Apps Deployment Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Environment:** ${{ needs.prepare.outputs.environment }}" >> $GITHUB_STEP_SUMMARY
echo "**Location:** ${{ env.AZURE_LOCATION }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ -f deployment-info.json ]; then
echo "### Deployment Details" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`json" >> $GITHUB_STEP_SUMMARY
cat deployment-info.json >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Job Results" >> $GITHUB_STEP_SUMMARY
echo "- Deploy: ${{ needs.build-and-deploy.result }}" >> $GITHUB_STEP_SUMMARY
echo "- Test: ${{ needs.test-deployment.result }}" >> $GITHUB_STEP_SUMMARY
if [ "${{ needs.build-and-deploy.result }}" == "success" ] && [ "${{ needs.test-deployment.result }}" == "success" ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "✅ **Deployment successful!**" >> $GITHUB_STEP_SUMMARY
else
echo "" >> $GITHUB_STEP_SUMMARY
echo "❌ **Deployment failed. Check logs for details.**" >> $GITHUB_STEP_SUMMARY
fi