Spaces:
Sleeping
Sleeping
| name: Deploy to Azure Container Apps | |
| # Manual-only β push-to-main now deploys to Hugging Face Spaces | |
| # (see deploy-huggingface.yml). Use this workflow to refresh the Azure | |
| # fallback when needed (`gh workflow run "Deploy to Azure Container Apps"`). | |
| on: | |
| workflow_dispatch: | |
| env: | |
| RESOURCE_GROUP: medishield-rg | |
| ACR_NAME: medishieldacr27529 | |
| CONTAINER_APP_NAME: medishield-classifier | |
| IMAGE_TAG: ${{ github.sha }} | |
| permissions: | |
| id-token: write | |
| contents: read | |
| jobs: | |
| test: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v4 | |
| with: | |
| version: "latest" | |
| - name: Install dependencies | |
| run: uv sync --frozen | |
| - name: Run tests | |
| run: uv run python -m pytest tests/ -q --tb=short | |
| env: | |
| GOOGLE_API_KEY: dummy-key-for-tests | |
| LANGCHAIN_TRACING_V2: "false" | |
| deploy: | |
| needs: test # only deploy if tests pass | |
| runs-on: ubuntu-latest | |
| environment: production | |
| steps: | |
| - uses: actions/checkout@v4 | |
| # ββ Azure login via OIDC (no long-lived secrets) ββββββββββββββββββββββ | |
| - name: Azure login | |
| uses: azure/login@v2 | |
| with: | |
| client-id: ${{ secrets.AZURE_CLIENT_ID }} | |
| tenant-id: ${{ secrets.AZURE_TENANT_ID }} | |
| subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} | |
| # ββ Set up buildx for linux/amd64 cross-platform builds ββββββββββββββ | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| # ββ Build linux/amd64 image (load into local Docker for smoke test) ββ | |
| - name: Build image (linux/amd64) | |
| run: | | |
| docker buildx build --platform linux/amd64 \ | |
| -t ${{ env.ACR_NAME }}.azurecr.io/classifier:${{ env.IMAGE_TAG }} \ | |
| -t ${{ env.ACR_NAME }}.azurecr.io/classifier:latest \ | |
| --load . | |
| # ββ Pre-push smoke test: image actually starts and /health responds ββ | |
| - name: Smoke test image locally | |
| run: | | |
| docker run -d --name smoke -p 8000:8000 \ | |
| -e GOOGLE_API_KEY=dummy-key-for-smoke-test \ | |
| -e PYTHONUNBUFFERED=1 \ | |
| ${{ env.ACR_NAME }}.azurecr.io/classifier:${{ env.IMAGE_TAG }} | |
| for i in $(seq 1 30); do | |
| if curl -sf http://localhost:8000/health; then | |
| echo "β Local image /health OK" | |
| docker rm -f smoke | |
| exit 0 | |
| fi | |
| sleep 2 | |
| done | |
| echo "β Local image failed to become healthy" | |
| docker logs smoke | |
| docker rm -f smoke | |
| exit 1 | |
| # ββ Push to ACR (only after local smoke test passed) βββββββββββββββββ | |
| - name: Push image to ACR | |
| run: | | |
| ACR_PASSWORD=$(az acr credential show --name ${{ env.ACR_NAME }} --query "passwords[0].value" -o tsv) | |
| echo "$ACR_PASSWORD" | docker login ${{ env.ACR_NAME }}.azurecr.io \ | |
| --username ${{ env.ACR_NAME }} --password-stdin | |
| docker push ${{ env.ACR_NAME }}.azurecr.io/classifier:${{ env.IMAGE_TAG }} | |
| docker push ${{ env.ACR_NAME }}.azurecr.io/classifier:latest | |
| # ββ Update secrets in Container Apps βββββββββββββββββββββββββββββββββ | |
| - name: Update Container App secrets | |
| run: | | |
| SECRETS="google-api-key=${{ secrets.GOOGLE_API_KEY }}" | |
| if [ -n "${{ secrets.LANGCHAIN_API_KEY }}" ]; then | |
| SECRETS="$SECRETS langchain-api-key=${{ secrets.LANGCHAIN_API_KEY }}" | |
| fi | |
| az containerapp secret set \ | |
| --name ${{ env.CONTAINER_APP_NAME }} \ | |
| --resource-group ${{ env.RESOURCE_GROUP }} \ | |
| --secrets $SECRETS | |
| # ββ Rolling deploy to Container Apps ββββββββββββββββββββββββββββββββββ | |
| - name: Deploy to Container Apps | |
| run: | | |
| ENV_VARS="GOOGLE_API_KEY=secretref:google-api-key PYTHONUNBUFFERED=1" | |
| if [ -n "${{ secrets.LANGCHAIN_API_KEY }}" ]; then | |
| ENV_VARS="$ENV_VARS LANGCHAIN_TRACING_V2=true LANGCHAIN_API_KEY=secretref:langchain-api-key LANGCHAIN_PROJECT=medishield-classification" | |
| fi | |
| az containerapp update \ | |
| --name ${{ env.CONTAINER_APP_NAME }} \ | |
| --resource-group ${{ env.RESOURCE_GROUP }} \ | |
| --image ${{ env.ACR_NAME }}.azurecr.io/classifier:${{ env.IMAGE_TAG }} \ | |
| --set-env-vars $ENV_VARS | |
| # ββ Smoke test the live endpoint ββββββββββββββββββββββββββββββββββββββ | |
| - name: Smoke test live /health | |
| run: | | |
| APP_URL=$(az containerapp show \ | |
| --name ${{ env.CONTAINER_APP_NAME }} \ | |
| --resource-group ${{ env.RESOURCE_GROUP }} \ | |
| --query "properties.configuration.ingress.fqdn" -o tsv) | |
| curl --fail --retry 5 --retry-delay 10 "https://$APP_URL/health" | |
| echo "β /health OK" | |
| # ββ Functional test: POST /classify with a real document ββββββββββββ | |
| - name: Smoke test live /classify | |
| run: | | |
| APP_URL=$(az containerapp show \ | |
| --name ${{ env.CONTAINER_APP_NAME }} \ | |
| --resource-group ${{ env.RESOURCE_GROUP }} \ | |
| --query "properties.configuration.ingress.fqdn" -o tsv) | |
| SAMPLE=$(ls dataset/*.png 2>/dev/null | head -1) | |
| if [ -z "$SAMPLE" ]; then | |
| echo "No sample image in dataset/, skipping functional test" | |
| exit 0 | |
| fi | |
| HTTP_CODE=$(curl -s -o /tmp/resp.json -w "%{http_code}" \ | |
| -X POST "https://$APP_URL/classify" -F "files=@$SAMPLE") | |
| if [ "$HTTP_CODE" != "200" ]; then | |
| echo "β /classify returned $HTTP_CODE" | |
| cat /tmp/resp.json | |
| exit 1 | |
| fi | |
| echo "β /classify returned 200" | |
| cat /tmp/resp.json | |
| - name: Print app URL | |
| run: | | |
| APP_URL=$(az containerapp show \ | |
| --name ${{ env.CONTAINER_APP_NAME }} \ | |
| --resource-group ${{ env.RESOURCE_GROUP }} \ | |
| --query "properties.configuration.ingress.fqdn" -o tsv) | |
| echo "π Deployed: https://$APP_URL" | |