diff --git a/.github/workflows/deploy-orchestrator.yml b/.github/workflows/deploy-orchestrator.yml index 1eabafd8..803e5eb7 100644 --- a/.github/workflows/deploy-orchestrator.yml +++ b/.github/workflows/deploy-orchestrator.yml @@ -72,10 +72,8 @@ env: jobs: docker-build: + if: inputs.trigger_type == 'workflow_dispatch' && inputs.build_docker_image == true uses: ./.github/workflows/job-docker-build.yml - with: - trigger_type: ${{ inputs.trigger_type }} - build_docker_image: ${{ inputs.build_docker_image }} secrets: inherit deploy: diff --git a/.github/workflows/job-deploy-linux.yml b/.github/workflows/job-deploy-linux.yml index ae33e1a9..56c620fb 100644 --- a/.github/workflows/job-deploy-linux.yml +++ b/.github/workflows/job-deploy-linux.yml @@ -263,7 +263,7 @@ jobs: azd env set AZURE_ENV_IMAGE_TAG="$INPUT_IMAGE_TAG" if [[ "$INPUT_BUILD_DOCKER_IMAGE" == "true" ]]; then - ACR_NAME="${{ secrets.ACR_TEST_LOGIN_SERVER }}" + ACR_NAME="${{ vars.ACR_TEST_LOGIN_SERVER }}" azd env set AZURE_ENV_CONTAINER_REGISTRY_ENDPOINT="$ACR_NAME" echo "Set ACR name to: $ACR_NAME" else diff --git a/.github/workflows/job-deploy-windows.yml b/.github/workflows/job-deploy-windows.yml index 87e6989f..7601bbd9 100644 --- a/.github/workflows/job-deploy-windows.yml +++ b/.github/workflows/job-deploy-windows.yml @@ -268,7 +268,7 @@ jobs: # Set ACR name only when building Docker image if ("$env:INPUT_BUILD_DOCKER_IMAGE" -eq "true") { - $ACR_NAME = "${{ secrets.ACR_TEST_LOGIN_SERVER }}" + $ACR_NAME = "${{ vars.ACR_TEST_LOGIN_SERVER }}" azd env set AZURE_ENV_CONTAINER_REGISTRY_ENDPOINT="$ACR_NAME" Write-Host "Set ACR name to: $ACR_NAME" } else { diff --git a/.github/workflows/job-docker-build.yml b/.github/workflows/job-docker-build.yml index 2babcb77..ec17062c 100644 --- a/.github/workflows/job-docker-build.yml +++ b/.github/workflows/job-docker-build.yml @@ -1,27 +1,21 @@ -name: Docker Build Job +name: Build & Push Test Images (Feature Branch) on: workflow_call: - inputs: - trigger_type: - description: 'Trigger type (workflow_dispatch, pull_request, schedule)' - required: true - type: string - build_docker_image: - description: 'Build And Push Docker Image (Optional)' - required: false - default: false - type: boolean outputs: IMAGE_TAG: description: "Generated Docker Image Tag" value: ${{ jobs.docker-build.outputs.IMAGE_TAG }} + workflow_dispatch: + +permissions: + contents: read + id-token: write env: BRANCH_NAME: ${{ github.event.workflow_run.head_branch || github.head_ref || github.ref_name }} jobs: docker-build: - if: inputs.trigger_type == 'workflow_dispatch' && inputs.build_docker_image == true runs-on: ubuntu-latest environment: production outputs: @@ -55,7 +49,11 @@ jobs: subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - name: Login to Azure Container Registry - run: az acr login --name ${{ secrets.ACR_TEST_LOGIN_SERVER }} + shell: bash + run: | + # Extract registry name from login server (e.g., myacr.azurecr.io -> myacr) + ACR_NAME=$(echo "${{ vars.ACR_TEST_LOGIN_SERVER }}" | cut -d'.' -f1) + az acr login --name "$ACR_NAME" - name: Build and Push Cod Mod backend Docker image uses: docker/build-push-action@v7 @@ -66,8 +64,8 @@ jobs: file: docker/Backend.Dockerfile push: true tags: | - ${{ secrets.ACR_TEST_LOGIN_SERVER }}/cmsabackend:${{ steps.generate_docker_tag.outputs.IMAGE_TAG }} - ${{ secrets.ACR_TEST_LOGIN_SERVER }}/cmsabackend:${{ steps.generate_docker_tag.outputs.IMAGE_TAG }}_${{ github.run_number }} + ${{ vars.ACR_TEST_LOGIN_SERVER }}/cmsabackend:${{ steps.generate_docker_tag.outputs.IMAGE_TAG }} + ${{ vars.ACR_TEST_LOGIN_SERVER }}/cmsabackend:${{ steps.generate_docker_tag.outputs.IMAGE_TAG }}_${{ github.run_number }} - name: Build and Push Cod Mod frontend Docker image uses: docker/build-push-action@v7 @@ -78,8 +76,8 @@ jobs: file: docker/Frontend.Dockerfile push: true tags: | - ${{ secrets.ACR_TEST_LOGIN_SERVER }}/cmsafrontend:${{ steps.generate_docker_tag.outputs.IMAGE_TAG }} - ${{ secrets.ACR_TEST_LOGIN_SERVER }}/cmsafrontend:${{ steps.generate_docker_tag.outputs.IMAGE_TAG }}_${{ github.run_number }} + ${{ vars.ACR_TEST_LOGIN_SERVER }}/cmsafrontend:${{ steps.generate_docker_tag.outputs.IMAGE_TAG }} + ${{ vars.ACR_TEST_LOGIN_SERVER }}/cmsafrontend:${{ steps.generate_docker_tag.outputs.IMAGE_TAG }}_${{ github.run_number }} - name: Verify Docker Image Build shell: bash @@ -91,7 +89,7 @@ jobs: if: always() shell: bash run: | - ACR_NAME=$(echo "${{ secrets.ACR_TEST_LOGIN_SERVER }}") + ACR_NAME=$(echo "${{ vars.ACR_TEST_LOGIN_SERVER }}") echo "## 🐳 Docker Build Job Summary" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY diff --git a/azure.yaml b/azure.yaml index 79608216..63c1367c 100644 --- a/azure.yaml +++ b/azure.yaml @@ -15,6 +15,20 @@ parameters: baseUrl: type: string default: 'https://raw.githubusercontent.com/microsoft/Modernize-your-code-solution-accelerator' + +hooks: + preprovision: + windows: + run: | + Write-Host "" + Write-Host " ---------------------------------------------------------------------------------------------------" -ForegroundColor Yellow + Write-Host " Note: If deployment fails or you encounter an issue, please open an issue with the deployment logs." -ForegroundColor Yellow + Write-Host " https://github.com/microsoft/Modernize-your-code-solution-accelerator/issues" -ForegroundColor Cyan + Write-Host " ---------------------------------------------------------------------------------------------------" -ForegroundColor Yellow + Write-Host "" + shell: pwsh + interactive: true + deployment: mode: Incremental template: ./infra/main.bicep # Path to the main.bicep file inside the 'deployment' folder diff --git a/docs/DeploymentGuide.md b/docs/DeploymentGuide.md index a9fec1c9..61116cd5 100644 --- a/docs/DeploymentGuide.md +++ b/docs/DeploymentGuide.md @@ -279,6 +279,15 @@ azd config set provision.preflight off ```shell azd up ``` +**During deployment, you'll be prompted for:** +1. **Environment name** (e.g., "cmsaapp") - Must be 3-16 characters long, alphanumeric only +2. **Azure subscription** selection +3. **Azure region** - Select a region with available GPT model quota +4. **Resource group** selection (create new or use existing) + +**Expected Duration:** 9-14 minutes for default configuration (includes remotely building and pushing the container images) + +**⚠️ Deployment Issues:** If you encounter errors or timeouts, try a different region as there may be capacity constraints. For detailed error solutions, see our [Troubleshooting Guide](./TroubleShootingSteps.md). ## Step 5: Build and Push Container Images ### 5.1 Run Image Build Script @@ -297,16 +306,6 @@ azd up .\scripts\build_and_push_images.ps1 ``` -**During deployment, you'll be prompted for:** -1. **Environment name** (e.g., "cmsaapp") - Must be 3-16 characters long, alphanumeric only -2. **Azure subscription** selection -3. **Azure region** - Select a region with available GPT model quota -4. **Resource group** selection (create new or use existing) - -**Expected Duration:** 9-14 minutes for default configuration (includes remotely building and pushing the container images) - -**⚠️ Deployment Issues:** If you encounter errors or timeouts, try a different region as there may be capacity constraints. For detailed error solutions, see our [Troubleshooting Guide](./TroubleShootingSteps.md). - ### 5.2 Get Application URL After successful deployment: diff --git a/infra/main.bicep b/infra/main.bicep index cbcea0b9..461cf83b 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -775,7 +775,7 @@ module aiServices 'modules/ai-foundry/aifoundry.bicep' = { { principalId: appIdentity.outputs.principalId principalType: 'ServicePrincipal' - roleDefinitionIdOrName: 'Cognitive Services OpenAI Contributor' + roleDefinitionIdOrName: '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd' // Cognitive Services OpenAI User } { principalId: appIdentity.outputs.principalId @@ -864,7 +864,7 @@ module storageAccount 'modules/storageAccount.bicep' = { { principalId: appIdentity.outputs.principalId principalType: 'ServicePrincipal' - roleDefinitionIdOrName: 'Storage Blob Data Contributor' + roleDefinitionIdOrName: 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' // Storage Blob Data Contributor } ] enableTelemetry: enableTelemetry diff --git a/infra/main.json b/infra/main.json index e9af9da0..e1b3f341 100644 --- a/infra/main.json +++ b/infra/main.json @@ -5,8 +5,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.44.1.10279", - "templateHash": "4200460093633058559" + "version": "0.45.6.27763", + "templateHash": "2646851374670029839" }, "name": "Modernize Your Code Solution Accelerator", "description": "CSA CTO Gold Standard Solution Accelerator for Modernize Your Code. \r\n" @@ -5087,8 +5087,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.44.1.10279", - "templateHash": "12666388833640590711" + "version": "0.45.6.27763", + "templateHash": "278403134034191131" } }, "definitions": { @@ -13095,11 +13095,11 @@ }, "dependsOn": [ "applicationInsights", - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').agentSvc)]", - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').monitor)]", + "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').storageBlob)]", "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').ods)]", + "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').monitor)]", "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').oms)]", - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').storageBlob)]", + "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').agentSvc)]", "dataCollectionEndpoint", "logAnalyticsWorkspace", "virtualNetwork" @@ -26155,7 +26155,7 @@ { "principalId": "[reference('appIdentity').outputs.principalId.value]", "principalType": "ServicePrincipal", - "roleDefinitionIdOrName": "Cognitive Services OpenAI Contributor" + "roleDefinitionIdOrName": "5e0bd9bd-7b93-4f28-af87-19fc36ad61bd" }, { "principalId": "[reference('appIdentity').outputs.principalId.value]", @@ -26183,8 +26183,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.44.1.10279", - "templateHash": "7796390788683636674" + "version": "0.45.6.27763", + "templateHash": "17695503720766712143" }, "name": "AI Services and Project Module", "description": "This module creates an AI Services resource and an AI Foundry project within it. It supports private networking, OpenAI deployments, and role assignments." @@ -27481,8 +27481,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.44.1.10279", - "templateHash": "15798298565583456780" + "version": "0.45.6.27763", + "templateHash": "5349372727764638433" } }, "definitions": { @@ -29191,8 +29191,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.44.1.10279", - "templateHash": "13008301984486295222" + "version": "0.45.6.27763", + "templateHash": "992342334639050267" } }, "definitions": { @@ -29406,8 +29406,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.44.1.10279", - "templateHash": "15798298565583456780" + "version": "0.45.6.27763", + "templateHash": "5349372727764638433" } }, "definitions": { @@ -31116,8 +31116,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.44.1.10279", - "templateHash": "13008301984486295222" + "version": "0.45.6.27763", + "templateHash": "992342334639050267" } }, "definitions": { @@ -32075,7 +32075,7 @@ { "principalId": "[reference('appIdentity').outputs.principalId.value]", "principalType": "ServicePrincipal", - "roleDefinitionIdOrName": "Storage Blob Data Contributor" + "roleDefinitionIdOrName": "ba92f5b4-2d11-453d-a403-e96b0029c9fe" } ] }, @@ -32090,8 +32090,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.44.1.10279", - "templateHash": "17237339939537137976" + "version": "0.45.6.27763", + "templateHash": "16329435452115246913" } }, "definitions": { @@ -40499,8 +40499,8 @@ }, "dependsOn": [ "appIdentity", - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').storageFile)]", "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').storageBlob)]", + "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').storageFile)]", "logAnalyticsWorkspace", "virtualNetwork" ] @@ -40544,8 +40544,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.44.1.10279", - "templateHash": "8524856574515803596" + "version": "0.45.6.27763", + "templateHash": "16140682139384730655" } }, "definitions": { diff --git a/infra/main_custom.bicep b/infra/main_custom.bicep index f33343e1..ca36126d 100644 --- a/infra/main_custom.bicep +++ b/infra/main_custom.bicep @@ -687,7 +687,7 @@ module aiServices 'modules/ai-foundry/aifoundry.bicep' = { { principalId: appIdentity.outputs.principalId principalType: 'ServicePrincipal' - roleDefinitionIdOrName: 'Cognitive Services OpenAI Contributor' + roleDefinitionIdOrName: '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd' // Cognitive Services OpenAI User } { principalId: appIdentity.outputs.principalId @@ -776,7 +776,7 @@ module storageAccount 'modules/storageAccount.bicep' = { { principalId: appIdentity.outputs.principalId principalType: 'ServicePrincipal' - roleDefinitionIdOrName: 'Storage Blob Data Contributor' + roleDefinitionIdOrName: 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' // Storage Blob Data Contributor } ] enableTelemetry: enableTelemetry diff --git a/src/backend/common/telemetry/patch_instrumentor.py b/src/backend/common/telemetry/patch_instrumentor.py index f2baca23..71e67c33 100644 --- a/src/backend/common/telemetry/patch_instrumentor.py +++ b/src/backend/common/telemetry/patch_instrumentor.py @@ -30,6 +30,54 @@ def _fixed_response_to_str(response_format: Any) -> Optional[str]: return str(response_format) +def _patch_fastapi_route_details(): + """ + Patch opentelemetry-instrumentation-fastapi's _get_route_details. + + The bug: _get_route_details() iterates over app.routes and reads + `starlette_route.path`. With FastAPI >= 0.119.0, app.include_router() + leaves an `_IncludedRouter` object in app.routes that has no `.path` + attribute. This raises: + AttributeError: '_IncludedRouter' object has no attribute 'path' + + It is triggered on requests where no concrete route matches FULL, + most notably CORS preflight `OPTIONS` requests from the frontend, + causing a 500 on the preflight and blocking the real request. + + The fix: replace the module-level _get_route_details with a version + that uses getattr(..., "path", None) and ignores routes whose + matches() raises. _get_default_span_details references this function + as a module global, so patching the module attribute is sufficient. + """ + try: + import opentelemetry.instrumentation.fastapi as fastapi_instr + from starlette.routing import Match + except ImportError: + logger.debug("opentelemetry.instrumentation.fastapi not installed, skipping patch") + return + + try: + def _safe_get_route_details(scope: Any) -> Optional[str]: + app = scope["app"] + route = None + for starlette_route in app.routes: + try: + match, _ = starlette_route.matches(scope) + except Exception: # noqa: BLE001 + continue + if match == Match.FULL: + route = getattr(starlette_route, "path", None) + break + if match == Match.PARTIAL: + route = getattr(starlette_route, "path", None) + return route + + fastapi_instr._get_route_details = _safe_get_route_details + logger.info("Patched opentelemetry.instrumentation.fastapi._get_route_details") + except Exception: # noqa: BLE001 + logger.exception("Failed to patch opentelemetry.instrumentation.fastapi route details") + + def patch_instrumentors(): """ Patch Azure AI telemetry instrumentors to handle dict response_format. @@ -60,3 +108,8 @@ def patch_instrumentors(): logger.info("Patched azure.ai.projects instrumentor") except ImportError: logger.debug("azure.ai.projects telemetry not installed, skipping patch") + + # Patch opentelemetry-instrumentation-fastapi to tolerate routes without a + # `.path` attribute (e.g. FastAPI >= 0.119.0 `_IncludedRouter`), which + # otherwise crashes span-name resolution on CORS preflight OPTIONS requests. + _patch_fastapi_route_details() diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json index 6cea4c6b..961d8c9e 100644 --- a/src/frontend/package-lock.json +++ b/src/frontend/package-lock.json @@ -19,7 +19,7 @@ "@tailwindcss/postcss": "^4.2.4", "@tailwindcss/vite": "^4.2.4", "autoprefixer": "^10.5.0", - "axios": "^1.15.2", + "axios": "^1.17.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^1.14.0", @@ -29,7 +29,7 @@ "react-dropzone": "^15.0.0", "react-icons": "^5.6.0", "react-redux": "^9.1.2", - "react-router-dom": "^7.14.2", + "react-router-dom": "^7.17.0", "react-syntax-highlighter": "^16.1.1", "sql-formatter": "^15.7.3", "tailwind-merge": "^3.5.0", @@ -3889,6 +3889,18 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ajv": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", @@ -4142,13 +4154,14 @@ } }, "node_modules/axios": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz", - "integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==", + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.17.0.tgz", + "integrity": "sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.11", + "follow-redirects": "^1.16.0", "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, @@ -4172,14 +4185,13 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "node_modules/browserslist": { @@ -4417,13 +4429,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -4550,7 +4555,6 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -5681,6 +5685,19 @@ "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==", "license": "CC0-1.0" }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -6718,7 +6735,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -7277,9 +7293,9 @@ } }, "node_modules/react-router": { - "version": "7.14.2", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.2.tgz", - "integrity": "sha512-yCqNne6I8IB6rVCH7XUvlBK7/QKyqypBFGv+8dj4QBFJiiRX+FG7/nkdAvGElyvVZ/HQP5N19wzteuTARXi5Gw==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.17.0.tgz", + "integrity": "sha512-FDELK7rTMlCHO5+reyXsPlmfr7N1F91lPHsWYfMEGQm/KQ+F4JFM8jGoeQDmDvdTs93Fw9aSilH+uKRb4/jXvQ==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -7299,12 +7315,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.14.2", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.2.tgz", - "integrity": "sha512-YZcM5ES8jJSM+KrJ9BdvHHqlnGTg5tH3sC5ChFRj4inosKctdyzBDhOyyHdGk597q2OT6NTrCA1OvB/YDwfekQ==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.17.0.tgz", + "integrity": "sha512-fyU2yjGups/hE6Xz0I5ZYbVL8Gx29eCjgpHaRaTaVU+OOAdfRX05KsvyRm0GO8YQwOkhpU3MurW1jyMUJn+zSw==", "license": "MIT", "dependencies": { - "react-router": "7.14.2" + "react-router": "7.17.0" }, "engines": { "node": ">=20.0.0" diff --git a/src/frontend/package.json b/src/frontend/package.json index 5df0430d..836b3e30 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -21,7 +21,7 @@ "@reduxjs/toolkit": "^2.11.2", "@tailwindcss/vite": "^4.2.4", "autoprefixer": "^10.5.0", - "axios": "^1.15.2", + "axios": "^1.17.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^1.14.0", @@ -32,7 +32,7 @@ "react-dropzone": "^15.0.0", "react-icons": "^5.6.0", "react-redux": "^9.1.2", - "react-router-dom": "^7.14.2", + "react-router-dom": "^7.17.0", "react-syntax-highlighter": "^16.1.1", "sql-formatter": "^15.7.3", "tailwind-merge": "^3.5.0", @@ -46,7 +46,8 @@ "minimatch": "^3.1.5", "flatted": "3.4.2", "js-yaml": "4.1.1", - "picomatch": "4.0.4" + "picomatch": "4.0.4", + "brace-expansion": "^2.0.1" }, "devDependencies": { "@eslint/js": "^9.39.2", diff --git a/src/tests/backend/app_test.py b/src/tests/backend/app_test.py index ea93d4e7..0f3030a8 100644 --- a/src/tests/backend/app_test.py +++ b/src/tests/backend/app_test.py @@ -30,7 +30,12 @@ async def test_health_check(app: FastAPI): @pytest.mark.asyncio async def test_backend_routes_exist(app: FastAPI): """Ensure /api routes are available (smoke test).""" - # Check available routes include /api prefix from backend_router - routes = [route.path for route in app.router.routes] - backend_routes = [r for r in routes if r.startswith("/api")] - assert backend_routes, "No backend routes found under /api prefix" + # Use the OpenAPI schema rather than walking `app.router.routes`, because + # newer FastAPI versions wrap included routers in an `_IncludedRouter` + # object (no `.path` attribute) which makes direct route iteration fragile. + # The OpenAPI `paths` mapping is the stable public surface and always + # contains the fully-resolved paths including the `/api` prefix added by + # `app.include_router(backend_router, prefix="/api", ...)`. + paths = app.openapi().get("paths", {}) + backend_paths = [p for p in paths if p.startswith("/api")] + assert backend_paths, "No backend routes found under /api prefix"