From d341666df56fb77752a40d9702510bb3dc8315b6 Mon Sep 17 00:00:00 2001 From: Paul Lizer Date: Wed, 4 Nov 2026 19:04:26 -0500 Subject: [PATCH] Consolidate Dependabot updates for React V2 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../workflows/broken-access-control-check.yml | 2 +- .../broken-access-control-full-scan.yml | 4 +- .github/workflows/codeql.yml | 6 +- .../malicious-pr-security-review.yml | 6 +- .github/workflows/python-syntax-check.yml | 2 +- .github/workflows/staging-azd-ui-tests.yml | 6 +- .github/workflows/swagger-route-check.yml | 2 +- .github/workflows/xss-sink-check.yml | 2 +- application/single_app/config.py | 2 +- application/single_app/requirements.txt | 18 +- .../CONSOLIDATED_DEPENDENCY_UPDATES_FIX.md | 77 ++++++++ .../test_dependency_upgrade_compatibility.py | 185 ++++++++++++++++++ requirements-dev.txt | 2 +- 13 files changed, 288 insertions(+), 26 deletions(-) create mode 100644 docs/explanation/fixes/CONSOLIDATED_DEPENDENCY_UPDATES_FIX.md create mode 100644 functional_tests/test_dependency_upgrade_compatibility.py diff --git a/.github/workflows/broken-access-control-check.yml b/.github/workflows/broken-access-control-check.yml index b78a4520e..e7e3ba7f8 100644 --- a/.github/workflows/broken-access-control-check.yml +++ b/.github/workflows/broken-access-control-check.yml @@ -26,7 +26,7 @@ jobs: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 with: python-version: '3.12' diff --git a/.github/workflows/broken-access-control-full-scan.yml b/.github/workflows/broken-access-control-full-scan.yml index dcbc05f32..86f84451b 100644 --- a/.github/workflows/broken-access-control-full-scan.yml +++ b/.github/workflows/broken-access-control-full-scan.yml @@ -29,7 +29,7 @@ jobs: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 with: python-version: '3.12' @@ -85,7 +85,7 @@ jobs: - name: Upload Broken Access Control full scan report if: always() && steps.python-files.outputs.file_count != '0' - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: broken-access-control-full-scan-report path: | diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 97a8d361b..290526e91 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -62,7 +62,7 @@ jobs: # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - name: Checkout repository - uses: actions/checkout@v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # Add any setup steps before running the `github/codeql-action/init` action. # This includes steps like installing compilers or runtimes (`actions/setup-node` @@ -72,7 +72,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v4 + uses: github/codeql-action/init@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -102,6 +102,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4 + uses: github/codeql-action/analyze@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/malicious-pr-security-review.yml b/.github/workflows/malicious-pr-security-review.yml index bd2e6b73f..912649e5a 100644 --- a/.github/workflows/malicious-pr-security-review.yml +++ b/.github/workflows/malicious-pr-security-review.yml @@ -69,12 +69,12 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v7 + uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 with: python-version: '3.12' @@ -141,7 +141,7 @@ jobs: - name: Upload review report if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: malicious-pr-security-review path: artifacts/malicious-pr-security-review.md diff --git a/.github/workflows/python-syntax-check.yml b/.github/workflows/python-syntax-check.yml index 215697ccb..915bee301 100644 --- a/.github/workflows/python-syntax-check.yml +++ b/.github/workflows/python-syntax-check.yml @@ -19,7 +19,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Python - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 with: python-version: '3.12' diff --git a/.github/workflows/staging-azd-ui-tests.yml b/.github/workflows/staging-azd-ui-tests.yml index 33f1521a2..3e8520c88 100644 --- a/.github/workflows/staging-azd-ui-tests.yml +++ b/.github/workflows/staging-azd-ui-tests.yml @@ -224,12 +224,12 @@ jobs: exit 1 - name: Set up Python - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 with: python-version: "3.12" - name: Set up Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: "20" cache: npm @@ -285,7 +285,7 @@ jobs: - name: Upload UI test artifacts if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: staging-ui-test-artifacts path: | diff --git a/.github/workflows/swagger-route-check.yml b/.github/workflows/swagger-route-check.yml index e22727573..ee7c552fb 100644 --- a/.github/workflows/swagger-route-check.yml +++ b/.github/workflows/swagger-route-check.yml @@ -28,7 +28,7 @@ jobs: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 with: python-version: '3.12' diff --git a/.github/workflows/xss-sink-check.yml b/.github/workflows/xss-sink-check.yml index 2e5b3fe93..349e9bcea 100644 --- a/.github/workflows/xss-sink-check.yml +++ b/.github/workflows/xss-sink-check.yml @@ -31,7 +31,7 @@ jobs: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 with: python-version: '3.12' diff --git a/application/single_app/config.py b/application/single_app/config.py index e958ee530..e39e74bbf 100644 --- a/application/single_app/config.py +++ b/application/single_app/config.py @@ -103,7 +103,7 @@ EXECUTOR_MAX_WORKERS = 30 CHAT_STREAM_EXECUTOR_MAX_WORKERS = max(8, int(os.getenv('SIMPLECHAT_CHAT_STREAM_WORKERS', '32') or '32')) SESSION_TYPE = 'filesystem' -VERSION = "0.261.401" +VERSION = "0.261.402" IS_DEVELOPMENT = is_development_env_enabled() # Opt-out for deployments where App Service Easy Auth is active but the platform diff --git a/application/single_app/requirements.txt b/application/single_app/requirements.txt index 082ec0434..e74aba6fe 100644 --- a/application/single_app/requirements.txt +++ b/application/single_app/requirements.txt @@ -4,7 +4,7 @@ azure-monitor-query==1.4.1 Flask==3.1.3 Flask-WTF==1.2.1 gunicorn==25.0.3 -Werkzeug==3.1.6 +Werkzeug==3.1.9 requests==2.33.1 jsonschema==4.25.1 regex==2026.5.9 @@ -30,7 +30,7 @@ python-dotenv==1.2.2 azure-ai-formrecognizer==3.3.3 azure-ai-projects==1.0.0 azure-ai-agents==1.2.0b6 -pyjwt==2.13.0 +pyjwt==2.15.0 markdown2==2.5.5 azure-mgmt-cognitiveservices==13.6.0 azure-identity==1.24.0 @@ -39,7 +39,7 @@ azure-storage-blob==12.24.1 azure-storage-file-share==12.25.0 azure-storage-queue==12.12.0 azure-keyvault-secrets==4.10.0 -pypdf==6.16.1 +pypdf==6.21.4 python-docx==1.1.2 docxcompose==2.2.0 python-pptx==1.0.2 @@ -49,16 +49,16 @@ langchain-text-splitters==1.1.2 beautifulsoup4==4.13.3 openpyxl==3.1.5 xlrd==2.0.1 -pillow==12.3.0 +pillow==13.0.0 ffmpeg-binaries-compat==1.0.1 ffmpeg-python==0.2.0 semantic-kernel==1.39.4 -snowflake-connector-python[pandas]==3.18.1 +snowflake-connector-python[pandas]==4.7.3 tableauserverclient==0.40 yamcs-client==2.1.0 protobuf==6.33.5 redis==5.3.1 -redis-entraid==1.2.1 +redis-entraid==1.2.2 smbprotocol==1.15.0 pyodbc==5.3.0 PyMySQL==1.1.2 @@ -66,9 +66,9 @@ azure-monitor-opentelemetry==1.8.9 psycopg2-binary==2.9.10 cython==3.2.4 pyyaml==6.0.2 -aiohttp==3.14.3 -# AnyIO 4.11+ requires typing_extensions.sentinel, which older App Service agents can shadow. -anyio==4.10.0 +aiohttp==3.14.4 +# This release no longer imports typing_extensions.sentinel (older App Service agents can shadow it). +anyio==4.14.2 html2text==2025.4.15 matplotlib==3.10.7 azure-cognitiveservices-speech==1.48.2 diff --git a/docs/explanation/fixes/CONSOLIDATED_DEPENDENCY_UPDATES_FIX.md b/docs/explanation/fixes/CONSOLIDATED_DEPENDENCY_UPDATES_FIX.md new file mode 100644 index 000000000..286684dee --- /dev/null +++ b/docs/explanation/fixes/CONSOLIDATED_DEPENDENCY_UPDATES_FIX.md @@ -0,0 +1,77 @@ +# Consolidated dependency updates (v0.261.402) + +## Issue and root cause + +Nine Dependabot PRs targeted `main`, while these updates need to enter through +`paullizer-react-v2-ui` and subsequently follow its promotion through Development +to main. A replacement PR collects the updates without bypassing that branch flow. +Closing an original PR as superseded does not mean its change has merged into main. + +The proposed PyJWT upgrade also conflicts with the feature branch's +`redis-entraid==1.2.1`, which requires `PyJWT~=2.13.0`. Merely copying the Dependabot +pins would leave the combined application requirements uninstallable. + +## Fixed in version: **0.261.402** + +`application/single_app/config.py` increments the application patch segment from +`0.261.401` to `0.261.402`. + +## Technical details + +| Original PR | Included update | +| --- | --- | +| #1866 | pypdf 6.16.1 to 6.21.4 | +| #1863 | SHA-pinned setup-python 7.1.0, setup-node 7.1.0, upload-artifact 7.0.2, checkout 7.0.1, and CodeQL 4.39.1 in the eight original workflows | +| #1862 | AnyIO 4.10.0 to 4.14.2 | +| #1829 | Pillow 12.3.0 to 13.0.0 | +| #1792 | snowflake-connector-python[pandas] 3.18.1 to 4.7.3 | +| #1791 | aiohttp 3.14.3 to 3.14.4 | +| #1782 | Development-only MCP SDK 1.28.1 to 1.30.0 | +| #1758 | Werkzeug 3.1.6 to 3.1.9 | +| #1599 | PyJWT 2.13.0 to 2.15.0 | + +The Python changes are in `application/single_app/requirements.txt` and +`requirements-dev.txt`. The directly coupled `redis-entraid` patch upgrade to +1.2.2 explicitly permits `PyJWT>=2.15.0,<3`; Redis, MSAL, and Azure Identity pins +remain unchanged. No deployment configuration or user-facing setting is added. + +The previous AnyIO pin protected App Service installations where an agent's +`typing_extensions` lacks `sentinel`. AnyIO 4.14.2 no longer imports that symbol. +The regression test removes it from the real module in a fresh process, then +imports the real AnyIO, OpenAI, MCP, and Semantic Kernel MCP integrations and +executes task groups, thread dispatch, and an HTTPX-mocked OpenAI completion. +This is checked with both normal and optimized Python. It does not claim +compatibility with arbitrary incompatible versions of agent-supplied packages. + +## Testing and impact + +`functional_tests/test_dependency_upgrade_compatibility.py` covers the historical +AnyIO import failure, PyJWT signature/audience validation and Redis token identity +extraction, Redis Entra package constraints, and real PNG/JPEG logo and ICO +conversion. Renamed GIF input must still fail the shared image-decoding allowlist. +The tests use isolated subprocesses rather than leaving mocked modules in the +pytest process. + +Local validation passed 54 focused pytest cases and all five standalone +Snowflake action checks on Python 3.12 in a virtual environment without inherited +system packages. The combined application/development requirements installed +successfully, and `python -m pip check` reported no broken requirements. +The three route-policy scripts passed +28 checks, and documentation coverage and site quality passed 13 checks. +All eight changed workflow files parsed successfully; their 21 relevant action +references match the expected immutable SHAs, verified against upstream tags +(including the dereferenced CodeQL release tag). All nine upgraded Python +packages, including the supporting Redis Entra patch, meet the repository's +seven-day release-age requirement. + +Existing focused tests cover PDF assembly and extraction, Snowflake action +configuration and read-only query handling, session merging, Redis token +providers, and MCP argument handling. Live Azure App Service, Redis, and +Snowflake authentication require deployment credentials and are not represented +by offline checks. The replacement PR records the exact validation commands, +results, and any environment limitations. + +Before this change, the dependency updates were split across main-targeting PRs +and the combined PyJWT/Redis Entra pins conflicted. After this change, all nine +updates are represented together against the feature branch, with the necessary +Redis Entra compatibility patch and regression coverage. diff --git a/functional_tests/test_dependency_upgrade_compatibility.py b/functional_tests/test_dependency_upgrade_compatibility.py new file mode 100644 index 000000000..5a248c877 --- /dev/null +++ b/functional_tests/test_dependency_upgrade_compatibility.py @@ -0,0 +1,185 @@ +# test_dependency_upgrade_compatibility.py +""" +Functional tests for consolidated Dependabot dependency compatibility. +Version: 0.261.402 +Implemented in: 0.261.402 + +Exercise real upgraded packages without Azure or Snowflake credentials. Fresh +processes isolate the simulated App Service typing_extensions shadowing from +other tests and exercise the asyncio backend in normal and optimized Python. +""" + +import subprocess +import sys +import textwrap +from pathlib import Path + +import pytest + + +ROOT = Path(__file__).resolve().parents[1] +APP_DIR = ROOT / "application" / "single_app" + + +def run_probe(source, *, optimized=False): + command = [sys.executable] + if optimized: + command.append("-O") + command.extend(["-c", textwrap.dedent(source)]) + result = subprocess.run( + command, + cwd=APP_DIR, + capture_output=True, + text=True, + timeout=60, + check=False, + ) + assert result.returncode == 0, result.stdout + result.stderr + + +@pytest.mark.parametrize("optimized", [False, True]) +def test_anyio_backend_without_typing_extensions_sentinel(optimized): + run_probe( + """ + import asyncio + import socket + import typing_extensions + + # Windows creates a loopback wakeup socketpair when constructing its event loop. + loop = asyncio.new_event_loop() + + def block_network(*args, **kwargs): + raise AssertionError("This compatibility probe must not access the network") + + socket.create_connection = block_network + socket.getaddrinfo = block_network + socket.socket.connect = block_network + socket.socket.connect_ex = block_network + if hasattr(typing_extensions, "sentinel"): + del typing_extensions.sentinel + + import anyio + import httpx + from openai import AsyncOpenAI + from mcp import ClientSession + from semantic_kernel.connectors.mcp import MCPStreamableHttpPlugin + + async def exercise_backend(): + values = [] + + async def append_from_thread(): + value = await anyio.to_thread.run_sync(lambda: 42) + values.append(value) + + async with anyio.create_task_group() as tasks: + tasks.start_soon(append_from_thread) + if values != [42]: + raise AssertionError("AnyIO thread/task behavior changed") + + def respond(request): + return httpx.Response(200, json={ + "id": "offline-completion", "object": "chat.completion", + "created": 0, "model": "offline-test", + "choices": [{"index": 0, "finish_reason": "stop", + "message": {"role": "assistant", "content": "ok"}}], + }) + + async with httpx.AsyncClient(transport=httpx.MockTransport(respond)) as http: + async with AsyncOpenAI(api_key="offline-test", http_client=http) as client: + response = await client.chat.completions.create( + model="offline-test", + messages=[{"role": "user", "content": "hello"}], + ) + if response.choices[0].message.content != "ok": + raise AssertionError("OpenAI/HTTPX response handling changed") + + anyio.run(exercise_backend, backend_options={"loop_factory": lambda: loop}) + """, + optimized=optimized, + ) + + +def test_pyjwt_and_redis_entra_dependency_compatibility(): + run_probe( + """ + from importlib.metadata import requires, version + import jwt + from packaging.requirements import Requirement + from redis_entraid.cred_provider import EntraIdCredentialsProvider + from functions_redis_client import RedisManagedIdentityCredentialProvider + + constraints = [ + Requirement(item) for item in requires("redis-entraid") + if Requirement(item).name.lower() == "pyjwt" + ] + if not constraints or not all( + version("PyJWT") in item.specifier for item in constraints + ): + raise AssertionError("Installed redis-entraid rejects the installed PyJWT") + + key = "offline-test-signing-key-at-least-32-bytes" + claims = {"sub": "offline-user", "oid": "offline-object", "aud": "simplechat"} + token = jwt.encode(claims, key, algorithm="HS256") + decoded = jwt.decode(token, key, algorithms=["HS256"], audience="simplechat") + if decoded != claims: + raise AssertionError("Signed JWT round-trip changed") + try: + jwt.decode(token, key, algorithms=["HS256"], audience="another-service") + except jwt.InvalidAudienceError: + pass + else: + raise AssertionError("JWT audience validation did not reject another service") + + class Credential: + def get_token(self, scope): + from types import SimpleNamespace + return SimpleNamespace(token=token, expires_on=4102444800) + + provider = RedisManagedIdentityCredentialProvider( + credential=Credential(), scope="https://redis.azure.com/.default" + ) + credentials = provider.get_credentials() + if credentials != ("offline-object", token): + raise AssertionError("Redis Entra object-id/token extraction changed") + """, + ) + + +def test_pillow_branding_conversion_with_upgraded_decoder(): + run_probe( + """ + from io import BytesIO + from PIL import Image, UnidentifiedImageError + from functions_branding_images import ( + prepare_favicon_image_for_storage, prepare_logo_image_for_storage, + ) + + for image_format in ("PNG", "JPEG"): + source = BytesIO() + Image.new("RGB", (200, 1000), "blue").save(source, format=image_format) + logo = prepare_logo_image_for_storage(source.getvalue(), "logo.png") + with Image.open(BytesIO(logo["png_data"])) as result: + result.load() + if result.format != "PNG" or result.size != (100, 500): + raise AssertionError("Logo conversion or aspect ratio changed") + favicon = prepare_favicon_image_for_storage(source.getvalue(), "favicon.png") + with Image.open(BytesIO(favicon["ico_data"])) as result: + result.load() + if result.format != "ICO" or result.size != (32, 32): + raise AssertionError("Favicon conversion changed") + + disguised = BytesIO() + Image.new("RGB", (32, 32)).save(disguised, format="GIF") + for prepare in (prepare_logo_image_for_storage, prepare_favicon_image_for_storage): + try: + prepare(disguised.getvalue(), "renamed.png") + except UnidentifiedImageError: + pass + else: + raise AssertionError("A renamed non-PNG/JPEG bypassed the decoding allowlist") + """, + ) + + +if __name__ == "__main__": + sys.exit(pytest.main([__file__, "-q"])) diff --git a/requirements-dev.txt b/requirements-dev.txt index 6cc40b558..9abd43643 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,6 +8,6 @@ pytest==9.0.3 pytest-playwright==0.7.2 azure-mgmt-playwright==1.0.0 -mcp==1.28.1 +mcp==1.30.0 starlette==1.3.1 uvicorn==0.50.2