Repository navigation
fix(db): widen purchase_history.account_id to VARCHAR(255) (#1677) #933
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 |