From 29cac3b0b3df9a986228cfe254ea8fd609e29e48 Mon Sep 17 00:00:00 2001 From: audit Date: Mon, 26 Oct 2026 23:28:07 +0900 Subject: [PATCH 1/3] ci(compat): test candidate wheel against oldest and latest SDK --- .github/workflows/ci-test.yml | 30 ++++++++++++++++++++------ tests/test_sdk_compatibility_policy.py | 20 +++++++++++++++++ 2 files changed, 43 insertions(+), 7 deletions(-) create mode 100644 tests/test_sdk_compatibility_policy.py diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index 257a301..a2eaa30 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -292,10 +292,14 @@ jobs: grep -F ">=3.11" <<<"$output" artifact-python311: - name: Wheel install and smoke (Py 3.11) + name: Wheel install and smoke (Py 3.11, ${{ matrix.sdk }}) needs: [changes, artifact-build] if: needs.changes.outputs.full_required == 'true' runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + sdk: ["azure-functions==1.21.0", "azure-functions>=1.21.0"] steps: - name: Checkout dependency metadata uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -315,7 +319,8 @@ jobs: run: | wheel=$(find compatibility-dist -maxdepth 1 -name '*.whl' -print -quit) test -n "$wheel" - python -m pip install "$wheel" + python -m pip install "$wheel" "${{ matrix.sdk }}" + python -m pip show azure-functions - name: Check installed wheel outside the checkout working-directory: /tmp @@ -343,15 +348,16 @@ jobs: PY host-smoke: - name: Real Functions host smoke (Py ${{ matrix.python-version }}) + name: Real Functions host smoke (Py ${{ matrix.python-version }}, ${{ matrix.sdk }}) runs-on: ubuntu-latest - needs: changes + needs: [changes, artifact-build] if: needs.changes.outputs.full_required == 'true' timeout-minutes: 5 strategy: fail-fast: false matrix: python-version: ["3.11", "3.12"] + sdk: ["azure-functions==1.21.0", "azure-functions>=1.21.0"] steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -370,9 +376,17 @@ jobs: npm install --global azure-functions-core-tools@4 --unsafe-perm true npm install --global azurite + - name: Download candidate wheel + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: compatibility-dist + path: compatibility-dist + - name: Install smoke app dependencies - working-directory: tests/host_smoke - run: pip install -r requirements.txt + run: | + python -m pip install compatibility-dist/*.whl "${{ matrix.sdk }}" + python -m pip check + python -m pip show azure-functions - name: Start Azurite and Functions host run: | @@ -384,9 +398,11 @@ jobs: run: | for attempt in {1..30}; do if response="$(curl --fail --silent http://127.0.0.1:7071/api/smoke)" \ - && default_response="$(curl --fail --silent http://127.0.0.1:7071/api/smoke-default)"; then + && default_response="$(curl --fail --silent http://127.0.0.1:7071/api/smoke-default)" \ + && contract_response="$(curl --fail --silent http://127.0.0.1:7071/api/smoke-contract)"; then test "$response" = "smoke" test "$default_response" = "smoke_default" + test "$contract_response" = "sdk-contract-ok" exit 0 fi sleep 2 diff --git a/tests/test_sdk_compatibility_policy.py b/tests/test_sdk_compatibility_policy.py new file mode 100644 index 0000000..4b24b0c --- /dev/null +++ b/tests/test_sdk_compatibility_policy.py @@ -0,0 +1,20 @@ +from pathlib import Path +import tomllib + +import yaml + + +def test_wheel_and_host_smokes_select_oldest_and_latest_sdk() -> None: + root = Path(__file__).resolve().parents[1] + workflow = yaml.safe_load((root / ".github/workflows/ci-test.yml").read_text()) + for name in ("artifact-python311", "host-smoke"): + job = workflow["jobs"][name] + assert job["strategy"]["matrix"]["sdk"] == [ + "azure-functions==1.21.0", + "azure-functions>=1.21.0", + ] + steps = job["steps"] + assert any("matrix.sdk" in step.get("run", "") for step in steps) + assert "artifact-build" in workflow["jobs"]["host-smoke"]["needs"] + project = tomllib.loads((root / "pyproject.toml").read_text()) + assert "azure-functions>=1.21.0" in project["project"]["dependencies"] From b6716cf7649716a47ef7afc50c607294b6b1dde1 Mon Sep 17 00:00:00 2001 From: audit Date: Mon, 26 Oct 2026 23:28:07 +0900 Subject: [PATCH 2/3] test(compat): assert invocation JSON and redaction in real host --- tests/host_smoke/function_app.py | 33 +++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/tests/host_smoke/function_app.py b/tests/host_smoke/function_app.py index b67a605..fd5f963 100644 --- a/tests/host_smoke/function_app.py +++ b/tests/host_smoke/function_app.py @@ -1,8 +1,13 @@ from __future__ import annotations +import io +import json +import logging + import azure.functions as func -from azure_functions_logging import with_context +from azure_functions_logging import JsonFormatter, attach_secure_redaction, with_context +from azure_functions_logging._context import ContextFilter app = func.FunctionApp() @@ -20,3 +25,29 @@ def smoke_default( context: func.Context = None, # type: ignore[assignment] ) -> func.HttpResponse: return func.HttpResponse(context.function_name) + + +@app.route(route="smoke-contract", auth_level=func.AuthLevel.ANONYMOUS) +@with_context(strict=True) +def smoke_contract(req: func.HttpRequest, context: func.Context) -> func.HttpResponse: + stream = io.StringIO() + handler = logging.StreamHandler(stream) + handler.setFormatter(JsonFormatter()) + handler.addFilter(ContextFilter()) + attach_secure_redaction(handler) + record = logging.getLogger("sdk-host-contract").makeRecord( + "sdk-host-contract", + logging.WARNING, + __file__, + 1, + "sdk host contract", + (), + None, + extra={"token": "SYNTHETIC_SDK_SECRET"}, + ) + handler.handle(record) + payload = json.loads(stream.getvalue()) + assert payload["invocation_id"] == context.invocation_id + assert payload["function_name"] == context.function_name + assert payload["extra"]["token"] == "***" + return func.HttpResponse("sdk-contract-ok") From 51825664983f136736472fed7478f6e80269f6f1 Mon Sep 17 00:00:00 2001 From: audit Date: Mon, 26 Oct 2026 23:28:07 +0900 Subject: [PATCH 3/3] docs(compat): define SDK range and forward-compatibility evidence --- docs/sdk-compatibility.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 docs/sdk-compatibility.md diff --git a/docs/sdk-compatibility.md b/docs/sdk-compatibility.md new file mode 100644 index 0000000..e9f3ac8 --- /dev/null +++ b/docs/sdk-compatibility.md @@ -0,0 +1,26 @@ +# Azure Functions SDK compatibility + +Python runtime support and SDK support are different contracts. This package +supports Python 3.11–3.14 and requires `azure-functions>=1.21.0`. + +The SDK bound is intentionally open. Required CI resolves both the exact floor +(`1.21.0`) and the newest stable release allowed by the requirement, installs +the candidate wheel, and runs import/structured-output smoke checks. The same +SDK selections run under a real local Functions host on Python 3.11 and 3.12, +including invocation context and secure redaction assertions. Pip's stable +resolver is used; prereleases are not certified implicitly. + +| Configuration | Evidence boundary | +| --- | --- | +| SDK 1.21.0, Python 3.11 | Candidate-wheel install/import and output smoke | +| Latest stable SDK >=1.21.0, Python 3.11 | Forward-compatibility candidate-wheel smoke; resolved version printed in CI | +| Both SDK selections, Python 3.11/3.12, Core Tools v4 | Candidate-wheel host registration, invocation fields and redaction | +| Python 3.13/3.14 | Offline library matrix; not a claim of host certification on those interpreters | +| Future SDK majors | Not certified before the forward-compatibility lane actually passes with that major | + +An open dependency bound is permission for the resolver, not a claim that every +future major has been tested. On a new-major failure, review the SDK change and +either adapt with regression tests or introduce a justified temporary ceiling +and upgrade plan. Do not silently call an untested major certified. Keep the +existing required CI and Azure release gates; local host success does not prove +Application Insights ingestion or cloud deployment compatibility.