From 0248246d2d592fa2d132ac68f9bebb28f977f64e Mon Sep 17 00:00:00 2001 From: devin-ai-keycard Date: Sat, 5 Sep 2026 01:34:40 +0000 Subject: [PATCH 1/2] ci: test every package at its keycardai-* sibling floors Adds a sibling-floors matrix job to pr.yml: per package, build the wheel, install it from PyPI with each keycardai-* dependency pinned to its declared floor (no workspace sources), and run the package's test suite there. A floor that is ahead of PyPI fails with its own message unless the PR carries the floors-bootstrap label. Raises keycardai-fastmcp's keycardai-oauth floor from 0.7.0 to 0.12.0, the oldest version where its suite passes under this job. Co-Authored-By: Larry Osakwe --- .github/workflows/pr.yml | 152 ++++++++++++++++++++++++++++++++ packages/fastmcp/pyproject.toml | 2 +- 2 files changed, 153 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index fcee62c..aea07e2 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -180,6 +180,158 @@ jobs: print(f"mcp {mcp_version}: keycardai-langchain, keycardai-mcp and langchain-mcp-adapters all import") PY + # Sibling floors for every package that depends on another keycardai-* + # package (ECO-379). The uv workspace resolves siblings to the local checkout, + # so lint-and-test runs each package against sibling code its published floor + # does not promise. This job builds the package wheel, installs it in a clean + # venv with each keycardai-* dependency pinned to its declared floor from + # PyPI, and runs the package's test suite there (import-smoke alone misses + # attribute reads that only a test exercises). One matrix leg per package so + # a failure in one does not hide another. + # + # Bootstrap escape hatch: a floor may name a sibling version that is not on + # PyPI yet, which happens when a carrier and its consumer merge together and + # the carrier has not released. That case fails with its own "ahead of PyPI" + # message. Add the `floors-bootstrap` label to the PR to turn it into a skip + # for the sequenced merge. The job never falls back to a newer version. + sibling-floors: + runs-on: ubuntu-latest + name: sibling floors (${{ matrix.package }}) + + strategy: + fail-fast: false + matrix: + package: [a2a, fastmcp, langchain, mcp, starlette, temporal] + + env: + PACKAGE: ${{ matrix.package }} + FLOORS_BOOTSTRAP: ${{ contains(github.event.pull_request.labels.*.name, 'floors-bootstrap') }} + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + # Tags drive uv-dynamic-versioning, so the built wheel carries the + # package's real version. + fetch-depth: 0 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install uv + uses: astral-sh/setup-uv@v4 + + - name: Build wheel + run: uv build --wheel "packages/$PACKAGE" --out-dir dist + + - name: Test at sibling floors + run: | + python - <<'PY' + import glob + import os + import re + import subprocess + import sys + import tomllib + import urllib.error + import urllib.request + + package = os.environ["PACKAGE"] + bootstrap = os.environ.get("FLOORS_BOOTSTRAP") == "true" + pkg_dir = f"packages/{package}" + + with open(f"{pkg_dir}/pyproject.toml", "rb") as f: + project = tomllib.load(f)["project"] + name = project["name"] + + floors = {} + for req in project["dependencies"]: + if not req.startswith("keycardai-"): + continue + m = re.fullmatch(r"(keycardai-[a-z0-9-]+)(\[[^\]]*\])?\s*>=\s*([0-9][0-9A-Za-z.]*)\s*(,.*)?", req) + if not m: + print(f"::error::{name}: cannot read a floor from sibling requirement {req!r}; expected keycardai-x>=A.B.C") + sys.exit(1) + floors[m.group(1)] = m.group(3) + + if not floors: + print(f"{name} declares no keycardai-* dependencies; nothing to check") + sys.exit(0) + + pins = [f"{sibling}=={floor}" for sibling, floor in floors.items()] + print(f"{name}: sibling floors {' '.join(pins)}") + + def on_pypi(sibling, version): + try: + urllib.request.urlopen(f"https://pypi.org/pypi/{sibling}/{version}/json").close() + return True + except urllib.error.HTTPError as e: + if e.code == 404: + return False + raise + + ahead = [(s, v) for s, v in floors.items() if not on_pypi(s, v)] + if ahead: + level = "warning" if bootstrap else "error" + for sibling, version in ahead: + print(f"::{level}::{name}: floor {sibling}>={version} is ahead of PyPI; {sibling} {version} is not published yet") + if bootstrap: + print("floors-bootstrap label is set on this PR; skipping the floors check for this package") + sys.exit(0) + print("Release the sibling first, or add the floors-bootstrap label to the PR for a sequenced merge.") + sys.exit(1) + + (wheel,) = glob.glob("dist/*.whl") + venv = ".venv-floors" + python = f"{venv}/bin/python" + subprocess.run(["uv", "venv", venv, "--python", "3.12"], check=True) + + # --no-sources keeps the siblings coming from the index at the pinned + # floor instead of the workspace checkout: the floor is what users install. + install = subprocess.run( + ["uv", "pip", "install", "--python", python, "--no-sources", f"{wheel}[test]", *pins], + capture_output=True, + text=True, + ) + sys.stdout.write(install.stdout) + sys.stderr.write(install.stderr) + if install.returncode != 0: + # uv reports resolver failures as a "×" headline followed by wrapped "╰─▶" detail. + lines = [line.strip() for line in install.stderr.splitlines() if line.strip()] + start = next((i for i, line in enumerate(lines) if line.startswith("×")), None) + if start is not None: + first = " ".join(line.lstrip("×╰─▶ ") for line in lines[start:]) + else: + first = next( + (line for line in lines if not line.startswith(("INFO ", "Using ", "Creating ", "Activate "))), + "install failed", + ) + print(f"::error::{name} does not install with {' '.join(pins)}: {first}") + sys.exit(1) + subprocess.run(["uv", "pip", "list", "--python", python], check=True) + + tests = subprocess.run( + [os.path.abspath(python), "-m", "pytest", "tests/", "-v", "-p", "no:cacheprovider"], + cwd=pkg_dir, + capture_output=True, + text=True, + ) + sys.stdout.write(tests.stdout) + sys.stderr.write(tests.stderr) + if tests.returncode != 0: + lines = tests.stdout.splitlines() + first = next( + (line for line in lines if line.startswith(("E ", "FAILED ", "ERROR "))), + f"pytest exited {tests.returncode}", + ) + print(f"::error::{name} fails its tests with {' '.join(pins)} (declared floors of {name}): {first.strip()}") + sys.exit(1) + + print(f"{name} passes its tests at sibling floors {' '.join(pins)}") + PY + release-preview: runs-on: ubuntu-latest needs: [validate-commits, lint-and-test] diff --git a/packages/fastmcp/pyproject.toml b/packages/fastmcp/pyproject.toml index a7cdecb..d326411 100644 --- a/packages/fastmcp/pyproject.toml +++ b/packages/fastmcp/pyproject.toml @@ -10,7 +10,7 @@ dependencies = [ "pydantic>=2.11.7", "pydantic-settings>=2.7.1", "httpx>=0.27.2", - "keycardai-oauth>=0.7.0", + "keycardai-oauth>=0.12.0", "fastmcp>=3.1.0", ] keywords = ["fastmcp", "mcp", "model-context-protocol", "oauth", "token-exchange", "authentication", "keycard"] From d54b380cec4f3a8c5e5df70a7aefa2ab4ff337e7 Mon Sep 17 00:00:00 2001 From: devin-ai-keycard Date: Sat, 5 Sep 2026 01:53:53 +0000 Subject: [PATCH 2/2] ci: raise keycardai-a2a and keycardai-mcp sibling floors to versions that pass at floor Both packages fail the sibling-floors job at their declared floors (mcp's pair is unsatisfiable, a2a needs KeycardAuthBackend(require_authentication=...)). The oldest passing pair for each is keycardai-oauth>=0.16.0 with keycardai-starlette>=0.9.0. uv.lock is unchanged: the workspace pins siblings to the local checkout. Co-Authored-By: Larry Osakwe --- packages/a2a/pyproject.toml | 4 ++-- packages/mcp/pyproject.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/a2a/pyproject.toml b/packages/a2a/pyproject.toml index 0e17894..107917f 100644 --- a/packages/a2a/pyproject.toml +++ b/packages/a2a/pyproject.toml @@ -7,8 +7,8 @@ requires-python = ">=3.10" license = { text = "MIT" } authors = [{ name = "Keycard", email = "support@keycard.ai" }] dependencies = [ - "keycardai-oauth>=0.11.0", - "keycardai-starlette>=0.3.0", + "keycardai-oauth>=0.16.0", + "keycardai-starlette>=0.9.0", "uvicorn[standard]>=0.32.0", "pydantic>=2.11.7", "httpx>=0.27.2", diff --git a/packages/mcp/pyproject.toml b/packages/mcp/pyproject.toml index 1e5b262..5d7bb65 100644 --- a/packages/mcp/pyproject.toml +++ b/packages/mcp/pyproject.toml @@ -7,8 +7,8 @@ requires-python = ">=3.10" license = { text = "MIT" } authors = [{ name = "Keycard", email = "support@keycard.ai" }] dependencies = [ - "keycardai-oauth>=0.9.0", - "keycardai-starlette>=0.6.0", + "keycardai-oauth>=0.16.0", + "keycardai-starlette>=0.9.0", "mcp>=2.0.0,<3.0", "pydantic>=2.11.7", "httpx>=0.27.2",