From 69d49bbadf3e875b67f8c3563dd7e9c62f14de9b Mon Sep 17 00:00:00 2001 From: Max Parke Date: Wed, 17 Jun 2026 12:57:41 -0400 Subject: [PATCH 1/2] ci: add release-please for versioned, self-describing contract releases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit scale-agentex has no version axis of its own — it floats at head, and the only version concept comes from SGP's platform release. That leaves no way to name an "oldest supported server contract" for downstream consumers. Add release-please (release-type: python; conventional-commit PR titles already enforced) so merges to main cut versioned releases: a vX.Y.Z git tag + GitHub release + CHANGELOG. Make the contract self-describe its version so the tag and spec never drift: - src/_version.py is the single source; the FastAPI app reads it (version=__version__), so generate_openapi_spec.py emits it as info.version. - release-please bumps src/_version.py, agentex/openapi.yaml (info.version), agentex/pyproject.toml, and the root pyproject together with the tag. Each tag is thus an immutable, self-describing snapshot of agentex/openapi.yaml the SDK's cross-version compat suite can pin as min-supported (scaleapi/scale-agentex-python#407), replacing the placeholder commit SHA today. Contract checkpoints, not a deploy gate: the server still floats at head for deploys. Seeded at 0.1.0 with bootstrap-sha at current main HEAD so the changelog starts fresh; the first feat/fix merge (or workflow_dispatch) cuts the first release. Verified gen-openapi emits info.version from _version.py with no spec drift. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/release-please.yml | 24 +++++++++++++++++++++ .release-please-manifest.json | 3 +++ agentex/src/_version.py | 3 +++ agentex/src/api/app.py | 2 ++ release-please-config.json | 32 ++++++++++++++++++++++++++++ 5 files changed, 64 insertions(+) create mode 100644 .github/workflows/release-please.yml create mode 100644 .release-please-manifest.json create mode 100644 agentex/src/_version.py create mode 100644 release-please-config.json diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 00000000..7ad80591 --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,24 @@ +name: release-please + +# Cuts versioned contract-checkpoint releases (tag + GitHub release + CHANGELOG) +# from conventional commits. These are contract snapshots, NOT a deploy gate. + +on: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + release-please: + if: github.repository == 'scaleapi/scale-agentex' + runs-on: ubuntu-latest + steps: + - uses: googleapis/release-please-action@5c625bfb5d1ff62eadeeb3772007f7f66fdcf071 # v4.4.1 + with: + config-file: release-please-config.json + manifest-file: .release-please-manifest.json diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 00000000..466df71c --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.1.0" +} diff --git a/agentex/src/_version.py b/agentex/src/_version.py new file mode 100644 index 00000000..bd85479f --- /dev/null +++ b/agentex/src/_version.py @@ -0,0 +1,3 @@ +"""Single source of the agentex contract version (bumped by release-please).""" + +__version__ = "0.1.0" # x-release-please-version diff --git a/agentex/src/api/app.py b/agentex/src/api/app.py index 41a9eada..0131ad30 100644 --- a/agentex/src/api/app.py +++ b/agentex/src/api/app.py @@ -21,6 +21,7 @@ from fastapi.responses import JSONResponse from fastapi.staticfiles import StaticFiles +from src._version import __version__ from src.adapters.crud_store.exceptions import ItemDoesNotExist from src.adapters.http.adapter_httpx import HttpxGateway from src.api.authentication_middleware import AgentexAuthMiddleware @@ -108,6 +109,7 @@ async def lifespan(_: FastAPI): fastapi_app = FastAPI( title="Agentex API", + version=__version__, openapi_url="/openapi.json", docs_url="/swagger", redoc_url="/api", diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 00000000..45985166 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "include-v-in-tag": true, + "include-component-in-tag": false, + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": false, + "bootstrap-sha": "ab469df91bba043ea25356c89147d29f7df03bad", + "changelog-sections": [ + { "type": "feat", "section": "Features" }, + { "type": "fix", "section": "Bug Fixes" }, + { "type": "perf", "section": "Performance Improvements" }, + { "type": "revert", "section": "Reverts" }, + { "type": "docs", "section": "Documentation" }, + { "type": "refactor", "section": "Refactors" }, + { "type": "chore", "section": "Chores", "hidden": true }, + { "type": "test", "section": "Tests", "hidden": true }, + { "type": "ci", "section": "Continuous Integration", "hidden": true }, + { "type": "build", "section": "Build System", "hidden": true }, + { "type": "style", "section": "Styles", "hidden": true } + ], + "packages": { + ".": { + "release-type": "python", + "package-name": "agentex", + "extra-files": [ + "agentex/src/_version.py", + { "type": "yaml", "path": "agentex/openapi.yaml", "jsonpath": "$.info.version" }, + { "type": "toml", "path": "agentex/pyproject.toml", "jsonpath": "$.project.version" } + ] + } + } +} From 7ff95b4ff93dbd8bae2cd07bd24a3a4a017b2f49 Mon Sep 17 00:00:00 2001 From: Max Parke Date: Wed, 17 Jun 2026 14:34:18 -0400 Subject: [PATCH 2/2] feat(api): advertise contract version via X-Agentex-Version response header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds VersionHeaderMiddleware, which sets X-Agentex-Version: <__version__> on every response (sourced from src/_version.py, the release-managed version), so clients can detect a server whose contract version is incompatible with their build — consumed by the SDK's runtime check (scale-agentex-python#410, AGX1-367). Pure-ASGI middleware (robust for streaming); registered in app.py alongside the existing stack. Co-Authored-By: Claude Opus 4.8 --- agentex/src/api/app.py | 2 ++ agentex/src/api/version_header_middleware.py | 30 +++++++++++++++++++ .../api/test_version_header_middleware.py | 22 ++++++++++++++ 3 files changed, 54 insertions(+) create mode 100644 agentex/src/api/version_header_middleware.py create mode 100644 agentex/tests/unit/api/test_version_header_middleware.py diff --git a/agentex/src/api/app.py b/agentex/src/api/app.py index 0131ad30..ab0f4a32 100644 --- a/agentex/src/api/app.py +++ b/agentex/src/api/app.py @@ -43,6 +43,7 @@ task_retention, tasks, ) +from src.api.version_header_middleware import VersionHeaderMiddleware from src.config import dependencies from src.config.dependencies import ( GlobalDependencies, @@ -139,6 +140,7 @@ async def lifespan(_: FastAPI): # Add Authentication middleware fastapi_app.add_middleware(AgentexAuthMiddleware) fastapi_app.add_middleware(RequestLoggingMiddleware) +fastapi_app.add_middleware(VersionHeaderMiddleware) # Mount the MkDocs site docs_path = Path(__file__).parent.parent.parent / "docs" / "site" diff --git a/agentex/src/api/version_header_middleware.py b/agentex/src/api/version_header_middleware.py new file mode 100644 index 00000000..23b76559 --- /dev/null +++ b/agentex/src/api/version_header_middleware.py @@ -0,0 +1,30 @@ +"""ASGI middleware that advertises the server's contract version on responses.""" + +from __future__ import annotations + +from starlette.datastructures import MutableHeaders +from starlette.types import ASGIApp, Message, Receive, Scope, Send + +from src._version import __version__ + +VERSION_HEADER = "x-agentex-version" + + +class VersionHeaderMiddleware: + """Set `X-Agentex-Version` on every HTTP response so clients can detect a + server whose contract version is incompatible with their build.""" + + def __init__(self, app: ASGIApp) -> None: + self.app = app + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope["type"] != "http": + await self.app(scope, receive, send) + return + + async def send_with_version(message: Message) -> None: + if message["type"] == "http.response.start": + MutableHeaders(scope=message)[VERSION_HEADER] = __version__ + await send(message) + + await self.app(scope, receive, send_with_version) diff --git a/agentex/tests/unit/api/test_version_header_middleware.py b/agentex/tests/unit/api/test_version_header_middleware.py new file mode 100644 index 00000000..807f9301 --- /dev/null +++ b/agentex/tests/unit/api/test_version_header_middleware.py @@ -0,0 +1,22 @@ +"""Unit tests for VersionHeaderMiddleware — sets X-Agentex-Version on responses.""" + +import pytest +from src._version import __version__ +from src.api.version_header_middleware import VERSION_HEADER, VersionHeaderMiddleware +from starlette.applications import Starlette +from starlette.responses import PlainTextResponse +from starlette.routing import Route +from starlette.testclient import TestClient + + +@pytest.mark.unit +def test_sets_version_header_on_responses(): + def endpoint(request): + return PlainTextResponse("ok") + + wrapped = VersionHeaderMiddleware(Starlette(routes=[Route("/x", endpoint)])) + client = TestClient(wrapped) + + response = client.get("/x") + assert response.status_code == 200 + assert response.headers[VERSION_HEADER] == __version__