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..ab0f4a32 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 @@ -42,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, @@ -108,6 +110,7 @@ async def lifespan(_: FastAPI): fastapi_app = FastAPI( title="Agentex API", + version=__version__, openapi_url="/openapi.json", docs_url="/swagger", redoc_url="/api", @@ -137,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__ 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" } + ] + } + } +}