Skip to content
Draft
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
56 changes: 56 additions & 0 deletions .github/workflows/azure-static-web-apps.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: Azure Static Web Apps CI/CD

on:
# push:
# branches:
# - main
# pull_request:
# types: [opened, synchronize, reopened, closed]
# branches:
# - main

jobs:
build_and_deploy_job:
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
runs-on: ubuntu-latest
name: Build and Deploy Job
permissions:
checks: write
contents: read
id-token: write
pull-requests: write
steps:

- uses: actions/checkout@v3
with:
submodules: true
lfs: false

- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: deployments/stitch-frontend/package.json
cache: "npm"
cache-dependency-path: deployments/stitch-frontend/package-lock.json

- name: Build
run: make frontend-build
env:
VITE_API_URL: https://stitch-db-demo-02.politesand-d8480c35.westus2.azurecontainerapps.io/api/v1

- name: Install OIDC Client from Core Package
run: npm install @actions/core@1.6.0 @actions/http-client

- name: Deploy
id: builddeploy
uses: Azure/static-web-apps-deploy@v1
with:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_DEPLOY_TOKEN }}
repo_token: ${{ secrets.GITHUB_TOKEN }}
action: "upload"
###### Repository/Build Configurations - These values can be configured to match your app requirements. ######
# For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig
skip_app_build: true
app_location: "/deployments/stitch-frontend/dist" # App source code path
skip_api_build: true
###### End of Repository/Build Configurations ######
150 changes: 150 additions & 0 deletions deployments/api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# api

## Deployment: Azure Container App

### Prerequisites

- Working local Docker environment (verify with `docker compose up --build`)
- Azure CLI installed and logged in (`az login`)
- `ACRPush` role on the target Azure Container Registry
- Existing PostgreSQL database provisioned and initialized (see `deployments/db/README.md`)
- Auth0 tenant and application configured

### Canonical Deployment Values (Example)

Replace these with environment-appropriate values.

```
SUBSCRIPTION=RMI-PROJECT-STITCH-SUB
RESOURCE_GROUP=STITCH-DEV-RG
REGISTRY=testdemostitchacr.azurecr.io
CONTAINER_APP_NAME=stitch-db-demo-02
IMAGE_NAME=stitch-api
VERSION=0.0.2
POSTGRES_HOST=stitch-deploy-test.postgres.database.azure.com
AUTH_ISSUER=https://rmi-spd.us.auth0.com/
AUTH_AUDIENCE=https://stitch-api.local
```

---

### Build and Push Image

Build the API image:

```bash
docker compose build api
```

Find the built image:

```bash
docker image ls
```

Tag and push:

```bash
REGISTRY="testdemostitchacr.azurecr.io"
VERSION="0.0.2"

docker tag <IMAGE_ID> "$REGISTRY/stitch-api:$VERSION"
az acr login -n "$REGISTRY"
docker push "$REGISTRY/stitch-api:$VERSION"
```

---

### Create Container App (Azure Portal)

#### Basics

- Subscription: `RMI-PROJECT-STITCH-SUB`
- Resource Group: `STITCH-DEV-RG`
- Name: `stitch-db-demo-02`
- Optimize for Azure Functions: Unchecked
- Deployment source: Container image
- Container Apps environment: As appropriate

#### Container

- Image: `<REGISTRY>/stitch-api:<VERSION>`
- *Note on Auth*: This deploy assumes that the ACR has an admin user, as a workaround to developers not having permissions to grant `ACRPull` permissions to new resources.
- Workload Profile: Consumption
- CPU/Memory: 0.25 CPU / 0.5 GiB

Environment Variables:

```
LOG_LEVEL=info
POSTGRES_DB=stitch
POSTGRES_HOST=<POSTGRES_HOST>
POSTGRES_PORT=5432
POSTGRES_USER=stitch_app
POSTGRES_PASSWORD=*****
AUTH_DISABLED=false
AUTH_ISSUER=<AUTH_ISSUER>
AUTH_AUDIENCE=<AUTH_AUDIENCE>
AUTH_JWKS_URI=<AUTH_ISSUER>.well-known/jwks.json
```

#### Ingress

- Enabled
- Traffic: From anywhere

---

### Verify Deployment State

Get the "Application URL" from the resource overview.

Health check:

```
https://<CONTAINER_APP_URL>/api/v1/health
```

Expected:

```json
{"status":"ok"}
```

Authenticated endpoint test:

```
/api/v1/resources/2
```

Expected (no token):

```json
{"detail":"Missing Authorization header"}
```

This confirms:

- Container is running
- DB connection works
- Auth middleware is active

---

### Debugging

#### View Logs

Portal → Container App → Log Analytics → Logs → Switch to KQL Mode:

```KQL
ContainerAppConsoleLogs_CL
| project TimeGenerated, RevisionName_s, Stream_s, Log_s
```

#### Add New Revision

Edit container configuration.
Add or update environment variables.
Create revision and wait for rollout.

7 changes: 4 additions & 3 deletions deployments/api/src/stitch/api/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ async def get_token_claims(
detail="Invalid or expired token",
headers={"WWW-Authenticate": "Bearer"},
)
except AuthError:
except AuthError as e:
logger.warning("JWT validation failed: %s", e, exc_info=True)
raise HTTPException(
status_code=HTTP_401_UNAUTHORIZED,
detail="Invalid or expired token",
Expand Down Expand Up @@ -129,8 +130,8 @@ async def get_current_user(claims: Claims, uow: UnitOfWorkDep) -> User:
async with session.begin_nested():
user_model = UserModel(
sub=claims.sub,
name=claims.name or "",
email=claims.email or "",
name=claims.name,
email=claims.email,
)
session.add(user_model)
except IntegrityError:
Expand Down
10 changes: 5 additions & 5 deletions deployments/api/tests/test_auth_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import pytest
from sqlalchemy import select
from sqlalchemy.exc import NoResultFound


from stitch.auth import TokenClaims

Expand Down Expand Up @@ -101,7 +103,7 @@ async def test_updates_name_email_on_subsequent_login(
assert row.email == "new@example.com"

@pytest.mark.anyio
async def test_defaults_empty_string_for_missing_name(
async def test_error_when_missing_claim(
self,
integration_session_factory,
):
Expand All @@ -111,10 +113,8 @@ async def test_defaults_empty_string_for_missing_name(
)

async with UnitOfWork(integration_session_factory) as uow:
user = await get_current_user(claims, uow)

assert user.name == ""
assert user.email == "valid@example.com"
with pytest.raises(NoResultFound):
await get_current_user(claims, uow)

@pytest.mark.anyio
async def test_handles_concurrent_first_login(
Expand Down
Loading
Loading