Skip to content

Commit bef031b

Browse files
Merge branch 'main' into dependabot/pip/numpy-gte-2-and-lt-2.5
2 parents ab8dbfc + 3b950f2 commit bef031b

11 files changed

Lines changed: 150 additions & 12 deletions

File tree

‎.github/dependabot.yml‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,23 @@ updates:
6464
update-types: ["version-update:semver-major"]
6565
- dependency-name: "dagster"
6666
update-types: ["version-update:semver-major"]
67+
# mcp 2.x's lowlevel Server drops the list_tools/call_tool decorators
68+
# integrations/agentflow_integrations/mcp/server.py is built on (checked
69+
# against 2.2.0), so a major bump is a port, not an update. PR #244
70+
# proposed <3 and was closed on that basis; the ignore stops it coming
71+
# back weekly. Same NOTE above applies: the safety/pip-audit jobs still
72+
# scan mcp and fail on a CVE that only a major fixes.
73+
- dependency-name: "mcp"
74+
update-types: ["version-update:semver-major"]
75+
# pyiceberg-core is pinned to the release PyIceberg pairs with, not to
76+
# the newest one. PyIceberg 0.11.1 declares pyiceberg-core>=0.5.1,<0.9.0;
77+
# the only step up inside that window is 0.8.0, published as
78+
# requires-python <3.13, which the 3.11-3.13 resolution environments in
79+
# pyproject.toml rule out. Bumps only make sense when PyIceberg raises
80+
# its own range, and that arrives as a pyiceberg PR. PR #245 proposed
81+
# <0.11 and was closed on that basis.
82+
- dependency-name: "pyiceberg-core"
83+
update-types: ["version-update:semver-minor"]
6784

6885
# ── Python: Flink runtime image manifest ───────────────────────────
6986
# Not an extra of the root project (its beam chain can never co-install
@@ -120,6 +137,12 @@ updates:
120137
- dependencies
121138
- python
122139
- integrations
140+
ignore:
141+
# The other half of the mirrored mcp pin (see the root pip entry): the
142+
# ceiling is what the integration's decorators need, so leaving this
143+
# ecosystem unignored just reopens the same PR from the other directory.
144+
- dependency-name: "mcp"
145+
update-types: ["version-update:semver-major"]
123146

124147
# ── npm: TypeScript SDK ────────────────────────────────────────────
125148
- package-ecosystem: npm

‎.github/workflows/ci.yml‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ jobs:
247247
with:
248248
python-version: "3.11"
249249
- name: Install dependencies
250+
id: install
250251
run: |
251252
bash scripts/ci_sync.sh test-integrations
252253
- name: Verify SDK/integrations joint installability
@@ -264,22 +265,27 @@ jobs:
264265
- name: Enforce changed-code coverage
265266
run: diff-cover .artifacts/coverage/coverage.xml --compare-branch=origin/main --fail-under=80
266267
- name: Run quality validators coverage gate
268+
if: ${{ !cancelled() && steps.install.outcome == 'success' }}
267269
run: |
268270
python -m pytest tests/unit/test_validators.py -v --tb=short --cov=agentflow_runtime.quality.validators --cov-report=term-missing --cov-fail-under=90
269271
- name: Run freshness monitor coverage gate
272+
if: ${{ !cancelled() && steps.install.outcome == 'success' }}
270273
run: |
271274
python -m pytest tests/unit/test_freshness_monitor.py -v --tb=short --cov=agentflow_runtime.quality.monitors.freshness_monitor --cov-report=term-missing --cov-fail-under=90
272275
- name: Run event producer coverage gate
276+
if: ${{ !cancelled() && steps.install.outcome == 'success' }}
273277
run: |
274278
python -m pytest tests/unit/test_event_producer.py -v --tb=short --cov=agentflow_runtime.ingestion.producers.event_producer --cov-report=term-missing --cov-fail-under=90
275279
- name: Run SQL guard coverage gate
280+
if: ${{ !cancelled() && steps.install.outcome == 'success' }}
276281
run: |
277282
# Security-critical NL->SQL guard: validate_nl_sql (SELECT-only, no DML,
278283
# tenant table allow-list, recursive-CTE shadow reject). test_sql_guard
279284
# plus test_sql_guard_mutation give full module coverage (100% local);
280285
# the 90% gate keeps a 10pp regression cushion.
281286
python -m pytest tests/unit/test_sql_guard.py tests/unit/test_sql_guard_mutation.py -v --tb=short --cov=agentflow_runtime.serving.semantic_layer.sql_guard --cov-report=term-missing --cov-fail-under=90
282287
- name: Run rate limiter coverage gate
288+
if: ${{ !cancelled() && steps.install.outcome == 'success' }}
283289
run: |
284290
# Security-critical sliding-window rate limiter (Redis + in-memory
285291
# fail-closed fallback); local module coverage is 98% (only the optional
@@ -297,6 +303,7 @@ jobs:
297303
-k "not auth_middleware" -v --tb=short -p no:schemathesis -p no:cov
298304
python -m coverage report --include="*/serving/api/rate_limiter.py" --show-missing --fail-under=90
299305
- name: Run auth manager coverage gate
306+
if: ${{ !cancelled() && steps.install.outcome == 'success' }}
300307
run: |
301308
# Security-critical auth manager (key match/verify, tenant isolation,
302309
# rate-limit/failed-auth windows, rotation grace) and a mutmut target;
@@ -331,6 +338,7 @@ jobs:
331338
-p no:schemathesis
332339
python -m coverage report --include="*/serving/api/auth/manager.py" --show-missing --fail-under=90
333340
- name: Run key rotation coverage gate
341+
if: ${{ !cancelled() && steps.install.outcome == 'success' }}
334342
run: |
335343
# Security-critical key-rotation lifecycle (create/rotate/revoke,
336344
# grace-period scheduling, rotation status) and a mutmut target. Like
@@ -353,6 +361,7 @@ jobs:
353361
-p no:schemathesis
354362
python -m coverage report --include="*/serving/api/auth/key_rotation.py" --show-missing --fail-under=90
355363
- name: Run outbox coverage gate
364+
if: ${{ !cancelled() && steps.install.outcome == 'success' }}
356365
run: |
357366
# Security/reliability-critical at-least-once outbox dispatch loop
358367
# (delivery, retry/backoff, poison-to-failed, mark-sent transactions)
@@ -362,6 +371,7 @@ jobs:
362371
python -m coverage run -m pytest tests/unit/test_outbox_processor.py tests/unit/test_outbox_connection_guard.py -p no:schemathesis
363372
python -m coverage report --include="*/processing/outbox.py" --show-missing --fail-under=90
364373
- name: Run query package coverage gate
374+
if: ${{ !cancelled() && steps.install.outcome == 'success' }}
365375
run: |
366376
# The NL->SQL orchestration surface (engine, entity/metric/NL query
367377
# mixins, SQL builder) and a mutmut target set; the old single-file

‎.github/workflows/publish-pypi.yml‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,14 @@ jobs:
7979
env.write(f"IS_RC={str('-rc' in release_version).lower()}\n")
8080
PY
8181
82+
# build 1.5.1 was yanked upstream ("considers breaking changes, will
83+
# discuss re-releasing as a new major version"). A yanked release still
84+
# installs when pinned exactly, so this kept working -- it just meant
85+
# every published artifact came out of a withdrawn builder. 1.6.0 is the
86+
# supported successor; uv.lock carries the same version for the wheels
87+
# CI builds in python-compat.
8288
- name: Install build tooling
83-
run: python -m pip install --upgrade build==1.5.1 twine==6.2.0
89+
run: python -m pip install --upgrade build==1.6.0 twine==6.2.0
8490

8591
- name: Clean previous distributions
8692
run: rm -rf dist sdk/dist

‎CHANGELOG.md‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,61 @@ All notable changes to AgentFlow are documented in this file.
44

55
## [Unreleased]
66

7+
### CI — a red coverage gate no longer hides the gates behind it
8+
9+
* **Steps in a job stop at the first failure**, and `test-unit` runs nine
10+
per-module coverage gates in a row. While an earlier step was failing, the
11+
auth-manager gate never ran at all — which is how that module drifted from
12+
94% to 82% for two weeks with a gate supposedly holding it at 90%, and how
13+
the key-rotation gate reached 89.6% (passing only because coverage rounds
14+
before it compares). Both were found by running the gates by hand.
15+
* **Each gate now runs whenever the environment installed**
16+
(`if: !cancelled() && steps.install.outcome == 'success'`). They measure
17+
different modules and share nothing but the venv, so there was never a
18+
reason for one to gate the next. Every gate still fails the job on its own;
19+
the difference is that one run now reports all nine verdicts instead of
20+
stopping at the first.
21+
* **A ratchet keeps it that way.**
22+
`test_coverage_policy.py::test_every_coverage_gate_runs_even_after_an_earlier_one_fails`
23+
fails if a gate is added without the condition, or if the install step
24+
loses the `id` the condition refers to.
25+
26+
### Supply chain — releases were being built by a yanked builder
27+
28+
* **`build` 1.5.1 is yanked upstream** ("considers breaking changes, will
29+
discuss re-releasing as a new major version"), and it was pinned in two
30+
places: `uv.lock`, which builds the wheels CI smoke-tests in
31+
`python-compat`, and `.github/workflows/publish-pypi.yml`, which builds the
32+
artifacts that go to PyPI. A yanked release still installs when pinned
33+
exactly, so nothing failed — every `uv lock` just printed the warning and
34+
every release came out of a withdrawn builder.
35+
* **Both now use 1.6.0**, the supported successor. `requirements-docker.lock`
36+
is unchanged (`build` is a `dev` dependency, not part of the
37+
cloud/postgres export), and `tests/unit/test_publish_workflows.py` pins the
38+
new command so the two cannot drift apart.
39+
40+
### Dependencies — the two version ceilings that keep coming back are now explained
41+
42+
* **`mcp<2` is load-bearing.** mcp 2.x's lowlevel `Server` no longer defines
43+
`list_tools` or `call_tool` (checked against 2.2.0), and
44+
`integrations/agentflow_integrations/mcp/server.py` builds its whole surface
45+
out of `@server.list_tools()` and `@server.call_tool()`. Taking mcp 2 means
46+
porting that module first. Both halves of the mirrored pin
47+
(`pyproject.toml` and `integrations/pyproject.toml`) now say so.
48+
* **`pyiceberg-core<0.8` is not a stale ceiling.** PyIceberg 0.11.1 declares
49+
`pyiceberg-core>=0.5.1,<0.9.0`, so 0.8.0 is the only step up inside the
50+
pairing upstream supports — and 0.8.0 is the one release in the line
51+
published as `requires-python <3.13`, which the 3.11–3.13 resolution
52+
`environments` rule out. 0.9+ restored 3.13 but sits outside PyIceberg's
53+
declared window. Nothing in that window is installable, so the ceiling
54+
moves when PyIceberg raises its own.
55+
* **Dependabot stops re-proposing both.** `.github/dependabot.yml` ignores
56+
`mcp` majors (in the root *and* integrations ecosystems, which own the two
57+
halves of the same pin) and `pyiceberg-core` minors, each with the reason
58+
and the exit condition. The existing NOTE about `ignore` also suppressing
59+
security updates still applies: `safety` and `pip-audit` scan both
60+
packages and fail on a CVE that only a blocked bump would fix.
61+
762
### Quality — the key-rotation coverage gate was passing on a rounding margin
863

964
* **89.6% rounds to 90.** The gate on `serving/api/auth/key_rotation.py` ran

‎Dockerfile.api‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ COPY packaging /build/packaging
2121
# a reproducibility input for the artifact: with --no-isolation its only job is
2222
# installing these two pinned distributions.
2323
RUN pip install --no-cache-dir --upgrade pip \
24-
&& pip install --no-cache-dir build==1.5.1 hatchling==1.31.0 \
24+
&& pip install --no-cache-dir build==1.6.0 hatchling==1.31.0 \
2525
&& python -m build --wheel --no-isolation --outdir /tmp/dist
2626

2727
FROM python:3.11-slim@sha256:9c900dea9e8fb7e16277c179b555cc72d29a352dbc33cff48ad5a0412fd5bfc7

‎deploy/hf-space/Dockerfile‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ WORKDIR /src
3232
# Mirrors Dockerfile.api: pinned build backend, and --no-isolation so the pin is
3333
# what builds the wheel instead of a fresh PyPI download inside build's venv.
3434
RUN pip install --no-cache-dir --upgrade pip \
35-
&& pip install --no-cache-dir build==1.5.1 hatchling==1.31.0 \
35+
&& pip install --no-cache-dir build==1.6.0 hatchling==1.31.0 \
3636
&& python -m build --wheel --no-isolation --outdir /tmp/dist
3737

3838
FROM python:3.11-slim@sha256:9c900dea9e8fb7e16277c179b555cc72d29a352dbc33cff48ad5a0412fd5bfc7

‎integrations/pyproject.toml‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ dependencies = [
2626

2727
[project.optional-dependencies]
2828
mcp = [
29-
# MCP 2 removes the low-level Server decorators used by this integration.
29+
# mcp 2.x's lowlevel Server drops the list_tools/call_tool decorators
30+
# server.py is built on (checked against 2.2.0). Port the module before
31+
# moving this pin; the root pyproject mirrors it.
3032
"mcp>=1.0,<2",
3133
]
3234

‎pyproject.toml‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,14 @@ dependencies = [
4040
cloud = [
4141
"boto3>=1.35,<2",
4242
"pyiceberg>=0.7,<1",
43-
# PyIceberg 0.11 write paths require native transforms. Core 0.8 excludes
44-
# Python 3.13, while 0.7 provides a stable-ABI wheel across Python 3.11-3.13.
43+
# PyIceberg 0.11 write paths require native transforms, and 0.7 is the
44+
# only core release that can serve them here. PyIceberg 0.11.1 declares
45+
# pyiceberg-core>=0.5.1,<0.9.0, so 0.8.0 is the only step up inside the
46+
# pairing upstream supports -- and 0.8.0 is the one release in the line
47+
# published as requires-python <3.13, which the 3.11-3.13 resolution
48+
# environments below rule out. (0.9+ restored 3.13 but sits outside
49+
# PyIceberg's declared window; nothing in that window is installable.)
50+
# The ceiling moves when PyIceberg raises its own, not before.
4551
"pyiceberg-core>=0.7,<0.8",
4652
]
4753
postgres = [
@@ -77,8 +83,11 @@ integrations = [
7783
"llama-index-core>=0.12,<1",
7884
# Mirrors integrations/pyproject.toml [mcp]: locking it here lets CI
7985
# install the editable integrations package with --no-deps from the
80-
# frozen root resolution (audit F-05). MCP 2 removes the low-level
81-
# Server decorators the integration uses.
86+
# frozen root resolution (audit F-05). The ceiling is load-bearing:
87+
# mcp 2.x's lowlevel Server no longer defines list_tools or call_tool
88+
# (checked against 2.2.0), and integrations/.../mcp/server.py builds its
89+
# whole surface out of @server.list_tools() and @server.call_tool().
90+
# Taking mcp 2 means porting that module first, not moving this pin.
8291
"mcp>=1.0,<2",
8392
]
8493
contract = [

‎tests/unit/test_coverage_policy.py‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,3 +177,36 @@ def test_ci_has_scoped_query_package_coverage_gate() -> None:
177177
assert "coverage run" in gate_step["run"]
178178
assert "*/serving/semantic_layer/query/*" in gate_step["run"]
179179
assert "--fail-under=90" in gate_step["run"]
180+
181+
182+
def test_every_coverage_gate_runs_even_after_an_earlier_one_fails() -> None:
183+
"""A red gate must not hide the gates behind it.
184+
185+
Steps in a job stop at the first failure, so for two weeks the
186+
auth-manager gate never ran: an earlier step in test-unit was failing, and
187+
the module drifted from 94% to 82% with nothing to report it. Each gate
188+
measures a different module and depends on nothing the others produce, so
189+
each one runs whenever the environment installed -- and still fails the
190+
job on its own.
191+
"""
192+
workflow = yaml.safe_load(
193+
(PROJECT_ROOT / ".github" / "workflows" / "ci.yml").read_text(encoding="utf-8")
194+
)
195+
steps = workflow["jobs"]["test-unit"]["steps"]
196+
197+
install = next(step for step in steps if step.get("name") == "Install dependencies")
198+
assert install.get("id") == "install", "the gate condition references steps.install"
199+
200+
gates = [
201+
step
202+
for step in steps
203+
if str(step.get("name", "")).startswith("Run ")
204+
and str(step.get("name", "")).endswith(" coverage gate")
205+
]
206+
assert len(gates) >= 9, f"expected the per-module gates, found {len(gates)}"
207+
208+
for gate in gates:
209+
condition = gate.get("if")
210+
assert condition is not None, f"{gate['name']} has no if: condition"
211+
assert "!cancelled()" in condition, gate["name"]
212+
assert "steps.install.outcome == 'success'" in condition, gate["name"]

‎tests/unit/test_publish_workflows.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ def test_python_publish_checks_artifacts_with_pinned_tooling_before_upload():
121121
names = [step.get("name") for step in steps if isinstance(step, dict)]
122122
commands = _step_commands(workflow)
123123

124-
assert "python -m pip install --upgrade build==1.5.1 twine==6.2.0" in commands
124+
assert "python -m pip install --upgrade build==1.6.0 twine==6.2.0" in commands
125125
assert "python scripts/check_release_artifacts.py dist/* sdk/dist/*" in commands
126126
assert names.index("Reject unsafe release artifacts") < names.index(
127127
"Dry-run build and twine check"

0 commit comments

Comments
 (0)