From 2f3182e0b6fa323582255f7d105089a565cef93a Mon Sep 17 00:00:00 2001 From: Yash-Chindam Date: Sun, 4 Oct 2026 16:37:11 +0530 Subject: [PATCH] fix: take the package, image and chart version from the release tag Releases are tagged after merge, so no committed file could track them and everything reported 0.1.0. Read the version from git tags, pass it into the image build, and package the chart at the tag in the release workflow. Co-Authored-By: Claude Opus 5.5 --- .gitattributes | 3 +++ .github/workflows/cd.yml | 9 +++++++++ .github/workflows/release.yml | 11 +++++++++++ Dockerfile | 4 ++++ README.md | 3 +++ deploy/helm/llm-routing/Chart.yaml | 6 ++++-- pyproject.toml | 14 ++++++++++++-- src/llm_router/__init__.py | 7 ++++++- src/llm_router/app.py | 3 ++- src/llm_router/chart.py | 25 +++++++++++-------------- tests/unit/test_chart.py | 5 +++++ 11 files changed, 70 insertions(+), 20 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..3bd84a9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +# One line ending everywhere, so generated files compare equal on any platform. +* text=auto eol=lf +*.png binary diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 1857704..f8693b8 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -28,10 +28,17 @@ jobs: - uses: actions/checkout@v7 with: ref: ${{ env.RELEASE_REF }} + fetch-depth: 0 + - name: Describe the version being packaged + # The tag for this commit is cut after CD starts, so this is the last + # release plus the distance from it, which is what the commit is. + id: version + run: echo "version=$(git describe --tags --always | sed 's/^v//')" >> "$GITHUB_OUTPUT" - uses: docker/setup-buildx-action@v4 - uses: docker/build-push-action@v7 with: context: . + build-args: VERSION=${{ steps.version.outputs.version }} outputs: type=oci,dest=${{ runner.temp }}/local-llm-router.tar tags: local-llm-router:${{ env.RELEASE_REF }} - uses: actions/upload-artifact@v7 @@ -65,6 +72,8 @@ jobs: - name: Verify the Helm chart matches the manifests run: python -m llm_router.chart --check && helm lint deploy/helm/llm-routing - name: Package the Helm chart + # The released chart, versioned by its tag, is attached to the GitHub + # release; this one is the unversioned build for inspection. run: helm package deploy/helm/llm-routing --destination chart - name: Verify every catalog adapter has a matching recipe run: python -m llm_router.adapters check diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 41fed88..0bfd8b3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -106,3 +106,14 @@ jobs: --title "$VERSION" \ --target "$RELEASE_SHA" \ --generate-notes + + - name: Attach the Helm chart for this version + if: steps.version.outputs.bump != 'none' + env: + VERSION: ${{ steps.version.outputs.version }} + run: | + set -euo pipefail + # The chart in the repository carries no version; the tag is the version. + helm package deploy/helm/llm-routing \ + --version "${VERSION#v}" --app-version "${VERSION#v}" --destination dist + gh release upload "$VERSION" dist/*.tgz --repo "${{ github.repository }}" --clobber diff --git a/Dockerfile b/Dockerfile index 21681d5..ced956e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,10 @@ FROM python:3.13-slim AS builder WORKDIR /build +# The build context carries no git history, so the release version is passed +# in; without it the image reports 0.0.0 rather than guessing. +ARG VERSION=0.0.0 +ENV SETUPTOOLS_SCM_PRETEND_VERSION=${VERSION} COPY pyproject.toml README.md ./ COPY src ./src RUN python -m pip wheel --no-cache-dir --wheel-dir /wheels . diff --git a/README.md b/README.md index 64a01a1..b4c7002 100644 --- a/README.md +++ b/README.md @@ -235,6 +235,9 @@ helm upgrade --install llm-routing deploy/helm/llm-routing \ | `gateway.replicas` | `2` | Starting gateway size. | | `gateway.autoscaling.minReplicas` / `maxReplicas` | `2` / `20` | KEDA bounds. | +Each GitHub release carries the chart packaged at that version. The chart in the repository +has no version of its own: like the Python package, it takes the release tag. + The chart is generated from the manifests and never edited by hand; tests check that it renders exactly what kustomize renders, in both modes. diff --git a/deploy/helm/llm-routing/Chart.yaml b/deploy/helm/llm-routing/Chart.yaml index 8b4be27..0d78b18 100644 --- a/deploy/helm/llm-routing/Chart.yaml +++ b/deploy/helm/llm-routing/Chart.yaml @@ -2,6 +2,8 @@ apiVersion: v2 name: llm-routing description: OpenAI-compatible gateway, policy router and local LLM serving plane. type: application -version: 0.1.0 -appVersion: "0.1.0" +# Set from the release tag when the chart is packaged: +# helm package --version --app-version +version: 0.0.0 +appVersion: "0.0.0" kubeVersion: ">=1.27.0-0" diff --git a/pyproject.toml b/pyproject.toml index 32f589a..d218583 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,10 @@ [build-system] -requires = ["hatchling"] +requires = ["hatchling", "hatch-vcs"] build-backend = "hatchling.build" [project] name = "local-llm-router" -version = "0.1.0" +dynamic = ["version"] description = "OpenAI-compatible gateway and policy router for local LLM inference." readme = "README.md" requires-python = ">=3.11" @@ -57,6 +57,16 @@ dev = [ "types-PyYAML>=6.0.12.20260906,<7", ] +# The version is the release tag. Releases are tagged by the release workflow +# after a merge, so no file in the repository has to be edited to cut one. +[tool.hatch.version] +source = "vcs" + +[tool.hatch.version.raw-options] +local_scheme = "no-local-version" +# Used when there is no git history to read, as in a source archive. +fallback_version = "0.0.0" + [tool.hatch.build.targets.wheel] packages = ["src/llm_router"] diff --git a/src/llm_router/__init__.py b/src/llm_router/__init__.py index 6838d70..80f5bf8 100644 --- a/src/llm_router/__init__.py +++ b/src/llm_router/__init__.py @@ -1,3 +1,8 @@ """Local LLM inference gateway and routing policy package.""" -__version__ = "0.1.0" +from importlib.metadata import PackageNotFoundError, version + +try: + __version__ = version("local-llm-router") +except PackageNotFoundError: # pragma: no cover - running from an uninstalled tree + __version__ = "0.0.0" diff --git a/src/llm_router/app.py b/src/llm_router/app.py index 8a4073e..4032620 100644 --- a/src/llm_router/app.py +++ b/src/llm_router/app.py @@ -14,6 +14,7 @@ from fastapi.responses import JSONResponse, StreamingResponse from opentelemetry.trace import TracerProvider +from llm_router import __version__ from llm_router.admission import ( AdmissionController, AdmissionRejectedError, @@ -259,7 +260,7 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]: app = FastAPI( title="Local LLM Inference Router", - version="0.1.0", + version=__version__, lifespan=lifespan, ) diff --git a/src/llm_router/chart.py b/src/llm_router/chart.py index d89395a..c1dc834 100644 --- a/src/llm_router/chart.py +++ b/src/llm_router/chart.py @@ -8,7 +8,6 @@ """ import re -import tomllib from collections.abc import Sequence from pathlib import Path from typing import Any @@ -178,25 +177,23 @@ def _values(source_root: Path) -> str: return "\n".join(lines) + "\n" -def _chart_metadata(source_root: Path) -> str: - project = tomllib.loads((source_root / "pyproject.toml").read_text(encoding="utf-8")) - version = project["project"]["version"] - return ( - "apiVersion: v2\n" - f"name: {CHART_NAME}\n" - "description: OpenAI-compatible gateway, policy router and local LLM serving plane.\n" - "type: application\n" - f"version: {version}\n" - f'appVersion: "{version}"\n' - 'kubeVersion: ">=1.27.0-0"\n' - ) +CHART_METADATA = f"""apiVersion: v2 +name: {CHART_NAME} +description: OpenAI-compatible gateway, policy router and local LLM serving plane. +type: application +# Set from the release tag when the chart is packaged: +# helm package --version --app-version +version: 0.0.0 +appVersion: "0.0.0" +kubeVersion: ">=1.27.0-0" +""" def build_chart(source_root: Path = Path(".")) -> dict[str, str]: """Every file of the chart, keyed by its path inside the chart directory.""" files = { - "Chart.yaml": _chart_metadata(source_root), + "Chart.yaml": CHART_METADATA, "values.yaml": _values(source_root), "templates/_helpers.tpl": HELPERS, "templates/dashboards.yaml": DASHBOARDS, diff --git a/tests/unit/test_chart.py b/tests/unit/test_chart.py index 59511fc..332a807 100644 --- a/tests/unit/test_chart.py +++ b/tests/unit/test_chart.py @@ -42,6 +42,11 @@ def resources(rendered: str) -> dict[Key, dict[str, Any]]: # Kustomize moves a patched variable to the front; order means nothing. for container in document["spec"]["template"]["spec"]["containers"]: container.get("env", []).sort(key=lambda item: item["name"]) + if document["kind"] == "ConfigMap": + # A Windows checkout may hold the same file with different line endings. + document["data"] = { + name: content.replace("\r\n", "\n") for name, content in document["data"].items() + } found[document["kind"], document["metadata"]["name"]] = document return found