Skip to content

Commit e5dac3d

Browse files
committed
✅ (ci): Add pre-publish framework-resolution guard
Resolves agent-assembly's declared runtime floors (read live from [project.dependencies]) together with each supported framework at its current release floor (AutoGen / CrewAI / Semantic Kernel), via `uv pip compile` as an offline-metadata solve. Fails the PR if a floor excludes a framework's supported range — catching the class of floor-vs-framework regression that shipped in rc.4 (AAASM-4518) BEFORE the immutable wheel is published, instead of after. Refs: AAASM-4518
1 parent b18bd0c commit e5dac3d

2 files changed

Lines changed: 175 additions & 0 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
name: Framework resolution check
2+
3+
# AAASM-4518 pre-publish guard: agent-assembly's runtime dependency floors must
4+
# stay co-installable with the supported agent frameworks (AutoGen / CrewAI /
5+
# Semantic Kernel). A floor raised above what a framework permits is a metadata-only
6+
# break that only surfaces AFTER the immutable wheel is published to PyPI — this job
7+
# turns that into a PR-time failure. See scripts/ci/check_framework_resolution.py.
8+
on:
9+
pull_request:
10+
paths:
11+
- "pyproject.toml"
12+
- "uv.lock"
13+
- "scripts/ci/check_framework_resolution.py"
14+
- ".github/workflows/framework-resolution-check.yml"
15+
push:
16+
branches: [master]
17+
paths:
18+
- "pyproject.toml"
19+
- "uv.lock"
20+
- "scripts/ci/check_framework_resolution.py"
21+
- ".github/workflows/framework-resolution-check.yml"
22+
23+
concurrency:
24+
group: ${{ github.workflow }}-${{ github.ref }}
25+
cancel-in-progress: true
26+
27+
permissions:
28+
contents: read
29+
30+
jobs:
31+
framework-floors-resolve:
32+
name: Dependency floors co-resolve with supported frameworks
33+
runs-on: ubuntu-latest
34+
steps:
35+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
36+
- uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v7
37+
- name: Resolve agent-assembly floors against the framework matrix
38+
run: python scripts/ci/check_framework_resolution.py --python-version 3.12
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
#!/usr/bin/env python3
2+
"""Pre-publish guard: agent-assembly's dependency floors must co-resolve with the
3+
supported agent frameworks.
4+
5+
WHY this exists (AAASM-4518): a well-meaning "pin the floor to the resolved
6+
version" bump (AAASM-4434) raised ``pydantic``/``protobuf`` floors above what
7+
CrewAI, Semantic Kernel, and AutoGen permit. Because the floors are metadata, the
8+
break only surfaced *after* the wheel was published to PyPI (rc.4 is immutable) —
9+
downstream users could no longer ``pip install agent-assembly`` alongside their
10+
framework. This guard turns that post-publish surprise into a PR-time failure.
11+
12+
It resolves the SDK's *declared runtime floors* (read live from
13+
``[project.dependencies]``) together with each framework at its current release
14+
floor, using ``uv pip compile`` as an offline-metadata SAT solve. It never builds
15+
or imports agent-assembly, so it is fast and runs on any platform. A floor that
16+
excludes a framework's supported range makes the union unsatisfiable and fails the
17+
guard, naming the conflicting constraint.
18+
19+
Usage::
20+
21+
python scripts/ci/check_framework_resolution.py # check current tree
22+
python scripts/ci/check_framework_resolution.py --python-version 3.12
23+
python scripts/ci/check_framework_resolution.py --pyproject path/to/pyproject.toml
24+
25+
Exit code 0 = every framework co-resolves; 1 = at least one conflict (details
26+
printed); 2 = harness error (uv missing, pyproject unreadable).
27+
"""
28+
29+
from __future__ import annotations
30+
31+
import argparse
32+
import subprocess
33+
import sys
34+
import tempfile
35+
import tomllib
36+
from pathlib import Path
37+
38+
# The frameworks agent-assembly ships quick-start examples for, each pinned at the
39+
# CURRENT release floor. The floor matters: an unconstrained `framework` lets the
40+
# resolver pick an ancient version that trivially resolves, hiding the very conflict
41+
# we guard against. Bump these floors when a framework's supported release moves.
42+
# (Mirrors the frameworks called out in AAASM-4518 / examples#268.)
43+
FRAMEWORK_MATRIX: list[str] = [
44+
"autogen-core>=0.7.5", # caps protobuf<5.30
45+
"crewai>=1.15.2", # caps pydantic<2.13
46+
"semantic-kernel>=1.30", # caps pydantic<2.12
47+
]
48+
49+
DEFAULT_PYTHON_VERSION = "3.12" # the SDK's minimum supported interpreter
50+
51+
52+
def read_runtime_floors(pyproject: Path) -> list[str]:
53+
"""Return the ``[project.dependencies]`` specifiers verbatim from pyproject.toml."""
54+
with pyproject.open("rb") as fh:
55+
data = tomllib.load(fh)
56+
deps = data.get("project", {}).get("dependencies")
57+
if not deps:
58+
raise SystemExit(f"error: no [project.dependencies] found in {pyproject}")
59+
# Drop self-referential extras (e.g. `agent-assembly[runtime]`) — they are not
60+
# third-party floors and would force a build of the local project.
61+
return [d for d in deps if not d.replace(" ", "").startswith("agent-assembly")]
62+
63+
64+
def resolve(reqs: list[str], python_version: str) -> tuple[bool, str]:
65+
"""Try to resolve ``reqs`` together. Returns (ok, combined resolver output)."""
66+
with tempfile.TemporaryDirectory() as tmp:
67+
req_in = Path(tmp) / "req.in"
68+
req_out = Path(tmp) / "req.txt"
69+
req_in.write_text("\n".join(reqs) + "\n")
70+
proc = subprocess.run(
71+
[
72+
"uv",
73+
"pip",
74+
"compile",
75+
str(req_in),
76+
"--python-version",
77+
python_version,
78+
"--output-file",
79+
str(req_out),
80+
"--quiet",
81+
"--no-header",
82+
],
83+
capture_output=True,
84+
text=True,
85+
)
86+
return proc.returncode == 0, (proc.stderr + proc.stdout).strip()
87+
88+
89+
def main() -> int:
90+
parser = argparse.ArgumentParser(description=__doc__)
91+
parser.add_argument(
92+
"--pyproject",
93+
default=str(Path(__file__).resolve().parents[2] / "pyproject.toml"),
94+
help="path to pyproject.toml (default: repo root)",
95+
)
96+
parser.add_argument(
97+
"--python-version",
98+
default=DEFAULT_PYTHON_VERSION,
99+
help=f"target interpreter for resolution (default: {DEFAULT_PYTHON_VERSION})",
100+
)
101+
args = parser.parse_args()
102+
103+
try:
104+
floors = read_runtime_floors(Path(args.pyproject))
105+
except (OSError, tomllib.TOMLDecodeError) as exc:
106+
print(f"error: cannot read {args.pyproject}: {exc}", file=sys.stderr)
107+
return 2
108+
109+
print("agent-assembly runtime floors under test:")
110+
for spec in floors:
111+
print(f" {spec}")
112+
print(f"target python: {args.python_version}\n")
113+
114+
failures: list[tuple[str, str]] = []
115+
for framework in FRAMEWORK_MATRIX:
116+
ok, output = resolve(floors + [framework], args.python_version)
117+
status = "OK " if ok else "FAIL"
118+
print(f"[{status}] agent-assembly floors + {framework}")
119+
if not ok:
120+
failures.append((framework, output))
121+
122+
if failures:
123+
print("\nFramework resolution conflicts (a floor excludes a supported framework):")
124+
for framework, output in failures:
125+
print(f"\n--- {framework} ---\n{output}")
126+
print(
127+
"\nRelax the offending floor to the widest range the SDK actually "
128+
"supports before publishing. See AAASM-4518."
129+
)
130+
return 1
131+
132+
print("\nAll frameworks co-resolve with the current dependency floors.")
133+
return 0
134+
135+
136+
if __name__ == "__main__":
137+
raise SystemExit(main())

0 commit comments

Comments
 (0)