From 41089a52c7efd9dc783fbc65468ca50cd363445e Mon Sep 17 00:00:00 2001 From: Tanner Leach Date: Tue, 21 Jul 2026 21:05:22 -0400 Subject: [PATCH] chore(release): prepare AI-Q 2.2.0 Set package and lock metadata to 2.2.0, update Compose defaults, and refresh Helm chart, image, archive, and installation references. Align published documentation and MCP release metadata with the final version. Add regression coverage that keeps package, Compose, Helm, packaged chart, and docs versions synchronized. Signed-off-by: Tanner Leach --- deploy/compose/README.md | 8 +- deploy/compose/docker-compose.yaml | 8 +- deploy/helm/README.md | 32 ++++---- deploy/helm/deployment-k8s/Chart.yaml | 4 +- .../helm/deployment-k8s/charts/aiq-0.0.5.tgz | Bin 10677 -> 10652 bytes deploy/helm/deployment-k8s/values.yaml | 4 +- .../aws-opensearch-serverless-values.yaml | 2 +- deploy/helm/helm-charts-k8s/aiq/Chart.yaml | 2 +- docs/source/deployment/docker-compose.md | 8 +- docs/source/deployment/kubernetes.md | 22 +++--- docs/source/project.json | 2 +- docs/source/versions1.json | 4 +- mcp/scripts/check_license_inventory.py | 2 +- mcp/tests/test_dependency_compatibility.py | 2 +- mcp/tests/test_release_checks.py | 10 +-- mcp/uv.lock | 2 +- pyproject.toml | 2 +- tests/deploy/test_compose_deployment.py | 47 ++++++++++++ tests/deploy/test_helm_deployment_k8s.py | 69 ++++++++++++++++++ uv.lock | 2 +- 20 files changed, 174 insertions(+), 58 deletions(-) diff --git a/deploy/compose/README.md b/deploy/compose/README.md index c841042a8..73cbc4921 100644 --- a/deploy/compose/README.md +++ b/deploy/compose/README.md @@ -108,16 +108,16 @@ cd deploy/compose docker login nvcr.io # Run with pre-built images -BACKEND_IMAGE=nvcr.io/nvidia/blueprint/aiq-agent:2.0.0 \ -FRONTEND_IMAGE=nvcr.io/nvidia/blueprint/aiq-frontend:2.0.0 \ +BACKEND_IMAGE=nvcr.io/nvidia/blueprint/aiq-agent:2.2.0 \ +FRONTEND_IMAGE=nvcr.io/nvidia/blueprint/aiq-frontend:2.2.0 \ docker compose --env-file ../.env -f docker-compose.yaml up -d ``` You can also add these to your `deploy/.env` file: ```bash -BACKEND_IMAGE=nvcr.io/nvidia/blueprint/aiq-agent:2.0.0 -FRONTEND_IMAGE=nvcr.io/nvidia/blueprint/aiq-frontend:2.0.0 +BACKEND_IMAGE=nvcr.io/nvidia/blueprint/aiq-agent:2.2.0 +FRONTEND_IMAGE=nvcr.io/nvidia/blueprint/aiq-frontend:2.2.0 ``` Then run without specifying them on the command line: diff --git a/deploy/compose/docker-compose.yaml b/deploy/compose/docker-compose.yaml index 77335d041..0aab8dd5b 100644 --- a/deploy/compose/docker-compose.yaml +++ b/deploy/compose/docker-compose.yaml @@ -18,8 +18,8 @@ # BUILD_TARGET=release docker compose --env-file ../.env -f docker-compose.yaml up -d --build # # # Use pre-built images from registry (skip --build) -# BACKEND_IMAGE=nvcr.io/nvidia/blueprint/aiq-agent:2.0.0 \ -# FRONTEND_IMAGE=nvcr.io/nvidia/blueprint/aiq-frontend:2.0.0 \ +# BACKEND_IMAGE=nvcr.io/nvidia/blueprint/aiq-agent:2.2.0 \ +# FRONTEND_IMAGE=nvcr.io/nvidia/blueprint/aiq-frontend:2.2.0 \ # docker compose --env-file ../.env -f docker-compose.yaml up -d # # Frontend image: @@ -29,7 +29,7 @@ services: aiq-agent: - image: ${BACKEND_IMAGE:-nvcr.io/nvidia/blueprint/aiq-agent:2.0.0} + image: ${BACKEND_IMAGE:-nvcr.io/nvidia/blueprint/aiq-agent:2.2.0} build: context: ../.. dockerfile: deploy/Dockerfile @@ -76,7 +76,7 @@ services: restart: unless-stopped frontend: - image: ${FRONTEND_IMAGE:-nvcr.io/nvidia/blueprint/aiq-frontend:2.0.0} + image: ${FRONTEND_IMAGE:-nvcr.io/nvidia/blueprint/aiq-frontend:2.2.0} build: context: ../../frontends/ui dockerfile: deploy/Dockerfile diff --git a/deploy/helm/README.md b/deploy/helm/README.md index 40636cb14..1c4518a53 100644 --- a/deploy/helm/README.md +++ b/deploy/helm/README.md @@ -31,7 +31,7 @@ All examples use `ns-aiq`. The repository source chart derives every namespaced resource from Helm's `.Release.Namespace`, supplied with `-n`; use a different namespace consistently across Helm, Secrets, `kubectl`, and external identity bindings. The `aiq.namespace.create` value controls whether the source chart renders a Namespace object -and does not override `-n`. The NGC instructions below install published chart 2.0.0. Use +and does not override `-n`. The NGC instructions below install published chart 2.2.0. Use the source chart when you need behavior from the checked-out repository. ## Prerequisites @@ -44,7 +44,7 @@ the source chart when you need behavior from the checked-out repository. ## Install from NGC Helm Repository -This section installs the pre-built chart **aiq2-web** version **2.0.0** from the NGC Helm repository (`https://helm.ngc.nvidia.com/nvidia/blueprint/charts/`). +This section installs the pre-built chart **aiq2-web** version **2.2.0** from the NGC Helm repository (`https://helm.ngc.nvidia.com/nvidia/blueprint/charts/`). ### 1. Create the namespace @@ -78,15 +78,15 @@ kubectl create secret docker-registry ngc-secret -n ns-aiq \ **Pull, verify, then install from local file:** ```bash -helm pull https://helm.ngc.nvidia.com/nvidia/blueprint/charts/aiq2-web-2.0.0.tgz \ +helm pull https://helm.ngc.nvidia.com/nvidia/blueprint/charts/aiq2-web-2.2.0.tgz \ --username='$oauthtoken' \ --password= # Verify the chart was pulled correctly -helm show chart aiq2-web-2.0.0.tgz +helm show chart aiq2-web-2.2.0.tgz # Install from the local file -helm upgrade --install aiq aiq2-web-2.0.0.tgz -n ns-aiq --create-namespace \ +helm upgrade --install aiq aiq2-web-2.2.0.tgz -n ns-aiq --create-namespace \ --wait --timeout 10m \ --set 'aiq.apps.backend.imagePullSecrets[0].name=ngc-secret' \ --set 'aiq.apps.frontend.imagePullSecrets[0].name=ngc-secret' \ @@ -99,7 +99,7 @@ helm upgrade --install aiq aiq2-web-2.0.0.tgz -n ns-aiq --create-namespace \ **Optional — Install directly from the chart URL** (without pulling first): ```bash -helm upgrade --install aiq https://helm.ngc.nvidia.com/nvidia/blueprint/charts/aiq2-web-2.0.0.tgz \ +helm upgrade --install aiq https://helm.ngc.nvidia.com/nvidia/blueprint/charts/aiq2-web-2.2.0.tgz \ --username='$oauthtoken' \ --password=$NGC_API_KEY \ -n ns-aiq --create-namespace \ @@ -114,7 +114,7 @@ helm upgrade --install aiq https://helm.ngc.nvidia.com/nvidia/blueprint/charts/a Pass additional `--set` flags to customize the deployment: ```bash -helm upgrade --install aiq aiq2-web-2.0.0.tgz -n ns-aiq \ +helm upgrade --install aiq aiq2-web-2.2.0.tgz -n ns-aiq \ --wait --timeout 10m \ --set 'aiq.apps.backend.imagePullSecrets[0].name=ngc-secret' \ --set 'aiq.apps.frontend.imagePullSecrets[0].name=ngc-secret' \ @@ -125,7 +125,7 @@ helm upgrade --install aiq aiq2-web-2.0.0.tgz -n ns-aiq \ Or supply a custom values file: ```bash -helm upgrade --install aiq aiq2-web-2.0.0.tgz -n ns-aiq \ +helm upgrade --install aiq aiq2-web-2.2.0.tgz -n ns-aiq \ --wait --timeout 10m \ -f custom-values.yaml ``` @@ -135,7 +135,7 @@ helm upgrade --install aiq aiq2-web-2.0.0.tgz -n ns-aiq \ To see what values the chart supports before installing: ```bash -helm show values aiq2-web-2.0.0.tgz +helm show values aiq2-web-2.2.0.tgz ``` ### Amazon OpenSearch Serverless @@ -220,10 +220,10 @@ Open [http://localhost:3000](http://localhost:3000) to access the web UI. ### Upgrade -To upgrade an existing release to a newer chart version, pull the new chart archive (same NGC URL pattern with the new version, e.g. `aiq2-web-2.0.1.tgz`) or use the new chart URL for direct install, then run: +To upgrade an existing release to a newer chart version, pull the new chart archive (same NGC URL pattern with the new version, e.g. `aiq2-web-.tgz`) or use the new chart URL for direct install, then run: ```bash -helm upgrade aiq aiq2-web-2.0.0.tgz -n ns-aiq \ +helm upgrade aiq aiq2-web-2.2.0.tgz -n ns-aiq \ --wait --timeout 10m \ --set 'aiq.apps.backend.imagePullSecrets[0].name=ngc-secret' \ --set 'aiq.apps.frontend.imagePullSecrets[0].name=ngc-secret' \ @@ -250,7 +250,7 @@ The backend loads a workflow config at startup. Switch configs with `--set`: | `configs/config_web_frag_mcp_auth.yml` | Foundational RAG with optional per-user MCP authentication | ```bash -helm upgrade --install aiq aiq2-web-2.0.0.tgz -n ns-aiq \ +helm upgrade --install aiq aiq2-web-2.2.0.tgz -n ns-aiq \ --wait --timeout 10m \ --set 'aiq.apps.backend.imagePullSecrets[0].name=ngc-secret' \ --set 'aiq.apps.frontend.imagePullSecrets[0].name=ngc-secret' \ @@ -281,7 +281,7 @@ aiq: Apply it to either the downloaded chart or a source-chart installation: ```bash -helm upgrade --install aiq aiq2-web-2.0.0.tgz -n ns-aiq \ +helm upgrade --install aiq aiq2-web-2.2.0.tgz -n ns-aiq \ --wait --timeout 10m \ -f aiq-per-user-auth-values.yaml ``` @@ -297,7 +297,7 @@ To use the Foundational RAG (FRAG) config, you need a running NVIDIA RAG Bluepri If the RAG Blueprint is deployed in the same Kubernetes cluster, use internal service DNS: ```bash -helm upgrade --install aiq aiq2-web-2.0.0.tgz -n ns-aiq \ +helm upgrade --install aiq aiq2-web-2.2.0.tgz -n ns-aiq \ --wait --timeout 10m \ --set 'aiq.apps.backend.imagePullSecrets[0].name=ngc-secret' \ --set 'aiq.apps.frontend.imagePullSecrets[0].name=ngc-secret' \ @@ -314,7 +314,7 @@ Replace `` with the namespace where the RAG Blueprint is deployed If the RAG service is running outside the cluster: ```bash -helm upgrade --install aiq aiq2-web-2.0.0.tgz -n ns-aiq \ +helm upgrade --install aiq aiq2-web-2.2.0.tgz -n ns-aiq \ --wait --timeout 10m \ --set 'aiq.apps.backend.imagePullSecrets[0].name=ngc-secret' \ --set 'aiq.apps.frontend.imagePullSecrets[0].name=ngc-secret' \ @@ -340,7 +340,7 @@ aiq: ``` ```bash -helm upgrade --install aiq aiq2-web-2.0.0.tgz -n ns-aiq \ +helm upgrade --install aiq aiq2-web-2.2.0.tgz -n ns-aiq \ --wait --timeout 10m \ -f aiq-frag-values.yaml ``` diff --git a/deploy/helm/deployment-k8s/Chart.yaml b/deploy/helm/deployment-k8s/Chart.yaml index 381c20f91..b619256c9 100644 --- a/deploy/helm/deployment-k8s/Chart.yaml +++ b/deploy/helm/deployment-k8s/Chart.yaml @@ -2,8 +2,8 @@ apiVersion: v2 name: aiq2-web description: AI-Q - WEB Profile () type: application -version: 2.1.1 -appVersion: '1.0' +version: 2.2.0 +appVersion: '2.2.0' dependencies: - name: aiq version: 0.0.5 diff --git a/deploy/helm/deployment-k8s/charts/aiq-0.0.5.tgz b/deploy/helm/deployment-k8s/charts/aiq-0.0.5.tgz index 22d514e0f077c9b290f2c3dc5af53f864aa3c527..09d80b3340028c26fb31e5ea37362f6bf57bc908 100644 GIT binary patch literal 10652 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 CA?4%% delta 10676 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 diff --git a/deploy/helm/deployment-k8s/values.yaml b/deploy/helm/deployment-k8s/values.yaml index 8ba558882..c686523f0 100644 --- a/deploy/helm/deployment-k8s/values.yaml +++ b/deploy/helm/deployment-k8s/values.yaml @@ -37,7 +37,7 @@ aiq: targetMemoryUtilizationPercentage: 80 image: repository: nvcr.io/nvidia/blueprint/aiq-agent - tag: "2.0.0" + tag: "2.2.0" pullPolicy: IfNotPresent imagePullSecrets: [] service: @@ -206,7 +206,7 @@ aiq: targetCPUUtilizationPercentage: 80 image: repository: nvcr.io/nvidia/blueprint/aiq-frontend - tag: "2.0.0" + tag: "2.2.0" pullPolicy: IfNotPresent imagePullSecrets: [] service: diff --git a/deploy/helm/examples/aws-opensearch-serverless-values.yaml b/deploy/helm/examples/aws-opensearch-serverless-values.yaml index 7eb320862..07f6b95bb 100644 --- a/deploy/helm/examples/aws-opensearch-serverless-values.yaml +++ b/deploy/helm/examples/aws-opensearch-serverless-values.yaml @@ -12,7 +12,7 @@ aiq: image: # Override this when testing a custom image that includes unreleased changes. repository: nvcr.io/nvidia/blueprint/aiq-agent - tag: "2.0.0" + tag: "2.2.0" pullPolicy: IfNotPresent imagePullSecrets: - name: ngc-image-pull-secret diff --git a/deploy/helm/helm-charts-k8s/aiq/Chart.yaml b/deploy/helm/helm-charts-k8s/aiq/Chart.yaml index 5c0f19f7d..65e8853f7 100644 --- a/deploy/helm/helm-charts-k8s/aiq/Chart.yaml +++ b/deploy/helm/helm-charts-k8s/aiq/Chart.yaml @@ -1,5 +1,5 @@ apiVersion: v2 -appVersion: 2.0.0 +appVersion: 2.2.0 description: AI-Q Helm Chart name: aiq type: application diff --git a/docs/source/deployment/docker-compose.md b/docs/source/deployment/docker-compose.md index 0b0d72c6f..00aba5e86 100644 --- a/docs/source/deployment/docker-compose.md +++ b/docs/source/deployment/docker-compose.md @@ -198,16 +198,16 @@ docker login nvcr.io # Run with pre-built images (no --build flag) cd deploy/compose -BACKEND_IMAGE=nvcr.io/nvidia/blueprint/aiq-agent:2.0.0 \ -FRONTEND_IMAGE=nvcr.io/nvidia/blueprint/aiq-frontend:2.0.0 \ +BACKEND_IMAGE=nvcr.io/nvidia/blueprint/aiq-agent:2.2.0 \ +FRONTEND_IMAGE=nvcr.io/nvidia/blueprint/aiq-frontend:2.2.0 \ docker compose --env-file ../.env -f docker-compose.yaml up -d ``` You can also add the image variables to `deploy/.env` instead of passing them on the command line: ```bash -BACKEND_IMAGE=nvcr.io/nvidia/blueprint/aiq-agent:2.0.0 -FRONTEND_IMAGE=nvcr.io/nvidia/blueprint/aiq-frontend:2.0.0 +BACKEND_IMAGE=nvcr.io/nvidia/blueprint/aiq-agent:2.2.0 +FRONTEND_IMAGE=nvcr.io/nvidia/blueprint/aiq-frontend:2.2.0 ``` ### Release Build diff --git a/docs/source/deployment/kubernetes.md b/docs/source/deployment/kubernetes.md index 6d5cfa282..d67deebd4 100644 --- a/docs/source/deployment/kubernetes.md +++ b/docs/source/deployment/kubernetes.md @@ -64,20 +64,20 @@ kubectl create secret docker-registry ngc-secret -n ns-aiq \ ### Using the NGC Helm chart -Install the pre-built chart **aiq2-web** version **2.0.0** from the NGC Helm repository (`https://helm.ngc.nvidia.com/nvidia/blueprint/charts/`). +Install the pre-built chart **aiq2-web** version **2.2.0** from the NGC Helm repository (`https://helm.ngc.nvidia.com/nvidia/blueprint/charts/`). **1. Pull the chart and install from local file:** ```bash -helm pull https://helm.ngc.nvidia.com/nvidia/blueprint/charts/aiq2-web-2.0.0.tgz \ +helm pull https://helm.ngc.nvidia.com/nvidia/blueprint/charts/aiq2-web-2.2.0.tgz \ --username='$oauthtoken' \ --password= # Verify the chart was pulled correctly -helm show chart aiq2-web-2.0.0.tgz +helm show chart aiq2-web-2.2.0.tgz # Install from the local file -helm upgrade --install aiq aiq2-web-2.0.0.tgz -n ns-aiq --create-namespace \ +helm upgrade --install aiq aiq2-web-2.2.0.tgz -n ns-aiq --create-namespace \ --wait --timeout 10m \ --set 'aiq.apps.backend.imagePullSecrets[0].name=ngc-secret' \ --set 'aiq.apps.frontend.imagePullSecrets[0].name=ngc-secret' @@ -88,7 +88,7 @@ Replace `` with your NGC API key (or use `$NGC_API_KEY`). Pref **2. Optional — Install directly from the chart URL** (without pulling first): ```bash -helm upgrade --install aiq https://helm.ngc.nvidia.com/nvidia/blueprint/charts/aiq2-web-2.0.0.tgz \ +helm upgrade --install aiq https://helm.ngc.nvidia.com/nvidia/blueprint/charts/aiq2-web-2.2.0.tgz \ --username='$oauthtoken' \ --password=$NGC_API_KEY \ -n ns-aiq --create-namespace \ @@ -109,7 +109,7 @@ but you can choose another namespace by replacing it consistently in Helm, `kube Secret creation, and external bindings such as EKS Pod Identity. The `aiq.namespace.create` value controls whether the chart renders a Namespace object; it does not override `-n`. This behavior applies to the repository source chart and does not -change the published NGC 2.0.0 instructions above. +change the published NGC 2.2.0 instructions above. ```bash cd deploy/helm @@ -194,7 +194,7 @@ The backend loads a workflow config at startup. Switch configs with `--set`: | `configs/config_web_frag.yml` | Foundational RAG mode (requires a running RAG service) | ```bash -helm upgrade --install aiq aiq2-web-2.0.0.tgz -n ns-aiq \ +helm upgrade --install aiq aiq2-web-2.2.0.tgz -n ns-aiq \ --wait --timeout 10m \ --set 'aiq.apps.backend.imagePullSecrets[0].name=ngc-secret' \ --set 'aiq.apps.frontend.imagePullSecrets[0].name=ngc-secret' \ @@ -212,7 +212,7 @@ To use the Foundational RAG (FRAG) config, you need a running NVIDIA RAG Bluepri If the RAG Blueprint is deployed in the same Kubernetes cluster, use internal service DNS: ```bash -helm upgrade --install aiq aiq2-web-2.0.0.tgz -n ns-aiq \ +helm upgrade --install aiq aiq2-web-2.2.0.tgz -n ns-aiq \ --wait --timeout 10m \ --set 'aiq.apps.backend.imagePullSecrets[0].name=ngc-secret' \ --set 'aiq.apps.frontend.imagePullSecrets[0].name=ngc-secret' \ @@ -228,7 +228,7 @@ Replace `` with the namespace where the RAG Blueprint is deployed If the RAG service is running outside the cluster: ```bash -helm upgrade --install aiq aiq2-web-2.0.0.tgz -n ns-aiq \ +helm upgrade --install aiq aiq2-web-2.2.0.tgz -n ns-aiq \ --wait --timeout 10m \ --set 'aiq.apps.backend.imagePullSecrets[0].name=ngc-secret' \ --set 'aiq.apps.frontend.imagePullSecrets[0].name=ngc-secret' \ @@ -253,7 +253,7 @@ aiq: ``` ```bash -helm upgrade --install aiq aiq2-web-2.0.0.tgz -n ns-aiq \ +helm upgrade --install aiq aiq2-web-2.2.0.tgz -n ns-aiq \ --wait --timeout 10m \ -f aiq-frag-values.yaml ``` @@ -301,7 +301,7 @@ kubectl rollout restart deployment -n ns-aiq aiq-backend aiq-frontend For NGC Helm chart releases, pull the new chart archive (same NGC URL pattern with the new version) if needed, then run: ```bash -helm upgrade aiq aiq2-web-2.0.0.tgz -n ns-aiq \ +helm upgrade aiq aiq2-web-2.2.0.tgz -n ns-aiq \ --wait --timeout 10m \ --set 'aiq.apps.backend.imagePullSecrets[0].name=ngc-secret' \ --set 'aiq.apps.frontend.imagePullSecrets[0].name=ngc-secret' diff --git a/docs/source/project.json b/docs/source/project.json index da158a78a..b04a97cd5 100644 --- a/docs/source/project.json +++ b/docs/source/project.json @@ -1,4 +1,4 @@ { "name": "NVIDIA AI-Q Blueprint", - "version": "2.2.0-rc1" + "version": "2.2.0" } diff --git a/docs/source/versions1.json b/docs/source/versions1.json index 6185c8440..84d99b234 100644 --- a/docs/source/versions1.json +++ b/docs/source/versions1.json @@ -1,8 +1,8 @@ [ { "preferred": true, - "version": "2.2.0-rc1", - "url": "https://docs.nvidia.com/aiq-blueprint/2.2.0-rc1/" + "version": "2.2.0", + "url": "https://docs.nvidia.com/aiq-blueprint/2.2.0/" }, { "version": "2.1.0", diff --git a/mcp/scripts/check_license_inventory.py b/mcp/scripts/check_license_inventory.py index 3699cd734..0a301c84b 100644 --- a/mcp/scripts/check_license_inventory.py +++ b/mcp/scripts/check_license_inventory.py @@ -30,7 +30,7 @@ } _FORBIDDEN_COMPONENTS = {"maas-sdk", "mos-sdk"} _LOCAL_SOURCE_COMPONENTS = { - ("aiq-agent", "2.0.0"): "../", + ("aiq-agent", "2.2.0"): "../", ("knowledge-layer", "1.0.0"): "../sources/knowledge_layer", ("tavily-web-search", "1.0.0"): "../sources/tavily_web_search", } diff --git a/mcp/tests/test_dependency_compatibility.py b/mcp/tests/test_dependency_compatibility.py index 2c3bc7946..d5a8a3573 100644 --- a/mcp/tests/test_dependency_compatibility.py +++ b/mcp/tests/test_dependency_compatibility.py @@ -27,7 +27,7 @@ def test_runtime_dependency_versions_are_the_validated_compatibility_baseline() assert version("mcp") == "1.28.1" assert version("nvidia-nat") == "1.8.0" assert version("nvidia-nat-core") == "1.8.0" - assert version("aiq-agent") == "2.0.0" + assert version("aiq-agent") == "2.2.0" assert version("cryptography") == "48.0.1" diff --git a/mcp/tests/test_release_checks.py b/mcp/tests/test_release_checks.py index 891d84f6d..753d5f2d2 100644 --- a/mcp/tests/test_release_checks.py +++ b/mcp/tests/test_release_checks.py @@ -181,7 +181,7 @@ def test_sbom_contract_accepts_the_exact_approved_local_component_set() -> None: "specVersion": "1.5", "metadata": {"component": {"name": "aiq-mcp-server", "version": "0.1.0"}}, "components": [ - {"name": "aiq-agent", "version": "2.0.0"}, + {"name": "aiq-agent", "version": "2.2.0"}, {"name": "knowledge-layer", "version": "1.0.0"}, {"name": "tavily-web-search", "version": "1.0.0"}, { @@ -201,7 +201,7 @@ def test_sbom_contract_requires_every_approved_local_component() -> None: "specVersion": "1.5", "metadata": {"component": {"name": "aiq-mcp-server", "version": "0.1.0"}}, "components": [ - {"name": "aiq-agent", "version": "2.0.0"}, + {"name": "aiq-agent", "version": "2.2.0"}, {"name": "knowledge-layer", "version": "1.0.0"}, ], } @@ -246,7 +246,7 @@ def test_sbom_contract_rejects_private_sdk_name_from_public_index() -> None: # representative public-PyPI registry dependency. Each test copies this baseline # and mutates a single entry to exercise one branch of the contract at a time. _APPROVED_LOCK_PACKAGES = [ - ("aiq-agent", "2.0.0", '{ editable = "../" }'), + ("aiq-agent", "2.2.0", '{ editable = "../" }'), ("knowledge-layer", "1.0.0", '{ editable = "../sources/knowledge_layer" }'), ("tavily-web-search", "1.0.0", '{ editable = "../sources/tavily_web_search" }'), ("aiq-mcp-server", "0.1.0", '{ editable = "." }'), @@ -310,7 +310,7 @@ def test_lock_sources_reject_non_editable_local_source(tmp_path: Path, source: s id="extra-unapproved-editable", ), pytest.param( - _without("aiq-agent") + [("aiq-agent", "2.0.0", '{ editable = "../wrong" }')], + _without("aiq-agent") + [("aiq-agent", "2.2.0", '{ editable = "../wrong" }')], id="approved-name-wrong-path", ), pytest.param( @@ -327,7 +327,7 @@ def test_lock_sources_reject_local_set_mismatch(tmp_path: Path, packages: list[t @pytest.mark.parametrize( ("name", "version", "path"), [ - ("aiq-agent", "2.0.0", ".."), + ("aiq-agent", "2.2.0", ".."), ("aiq-mcp-server", "0.1.0", "./"), ], ) diff --git a/mcp/uv.lock b/mcp/uv.lock index d38df1001..6b1a26259 100644 --- a/mcp/uv.lock +++ b/mcp/uv.lock @@ -168,7 +168,7 @@ wheels = [ [[package]] name = "aiq-agent" -version = "2.0.0" +version = "2.2.0" source = { editable = "../" } dependencies = [ { name = "aiofiles" }, diff --git a/pyproject.toml b/pyproject.toml index a71f2a472..7ea37fb06 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ package-dir = {"" = "src"} [project] name = "aiq-agent" -version = "2.0.0" +version = "2.2.0" description = "NVIDIA AI-Q Blueprint - An enterprise-grade agent for intelligent research assistance" readme = "README.md" requires-python = ">=3.11,<3.14" diff --git a/tests/deploy/test_compose_deployment.py b/tests/deploy/test_compose_deployment.py index 03034c676..4b1462fb6 100644 --- a/tests/deploy/test_compose_deployment.py +++ b/tests/deploy/test_compose_deployment.py @@ -2,8 +2,10 @@ # SPDX-License-Identifier: Apache-2.0 import json +import re import shutil import subprocess +import tomllib from pathlib import Path from typing import Any @@ -14,8 +16,14 @@ REPO_ROOT = Path(__file__).resolve().parents[2] COMPOSE_PATH = REPO_ROOT / "deploy" / "compose" / "docker-compose.yaml" +COMPOSE_README_PATH = REPO_ROOT / "deploy" / "compose" / "README.md" +DOCS_COMPOSE_PATH = REPO_ROOT / "docs" / "source" / "deployment" / "docker-compose.md" +DOCS_PROJECT_PATH = REPO_ROOT / "docs" / "source" / "project.json" +DOCS_VERSIONS_PATH = REPO_ROOT / "docs" / "source" / "versions1.json" +PYPROJECT_PATH = REPO_ROOT / "pyproject.toml" PER_USER_AUTH_COMPOSE_PATH = REPO_ROOT / "deploy" / "compose" / "docker-compose.per-user-auth.yaml" MCP_CONFIG_PATH = REPO_ROOT / "configs" / "config_web_frag_mcp_auth.yml" +EXPECTED_RELEASE_VERSION = "2.2.0" def load_compose() -> dict[str, Any]: @@ -23,6 +31,45 @@ def load_compose() -> dict[str, Any]: return yaml.safe_load(compose_file) +def test_release_version_matches_default_compose_images(): + with PYPROJECT_PATH.open("rb") as pyproject_file: + package_version = tomllib.load(pyproject_file)["project"]["version"] + + compose = load_compose() + + assert package_version == EXPECTED_RELEASE_VERSION + assert compose["services"]["aiq-agent"]["image"] == ( + f"${{BACKEND_IMAGE:-nvcr.io/nvidia/blueprint/aiq-agent:{package_version}}}" + ) + assert compose["services"]["frontend"]["image"] == ( + f"${{FRONTEND_IMAGE:-nvcr.io/nvidia/blueprint/aiq-frontend:{package_version}}}" + ) + + expected_backend_image = f"nvcr.io/nvidia/blueprint/aiq-agent:{package_version}" + expected_frontend_image = f"nvcr.io/nvidia/blueprint/aiq-frontend:{package_version}" + for release_path in (COMPOSE_PATH, COMPOSE_README_PATH, DOCS_COMPOSE_PATH): + release_text = release_path.read_text(encoding="utf-8") + backend_images = set(re.findall(r"nvcr\.io/nvidia/blueprint/aiq-agent:[\w.-]+", release_text)) + frontend_images = set(re.findall(r"nvcr\.io/nvidia/blueprint/aiq-frontend:[\w.-]+", release_text)) + assert backend_images == {expected_backend_image} + assert frontend_images == {expected_frontend_image} + + +def test_release_version_matches_published_docs(): + project_metadata = json.loads(DOCS_PROJECT_PATH.read_text(encoding="utf-8")) + published_versions = json.loads(DOCS_VERSIONS_PATH.read_text(encoding="utf-8")) + preferred_versions = [entry for entry in published_versions if entry.get("preferred")] + + assert project_metadata["version"] == EXPECTED_RELEASE_VERSION + assert preferred_versions == [ + { + "preferred": True, + "version": EXPECTED_RELEASE_VERSION, + "url": f"https://docs.nvidia.com/aiq-blueprint/{EXPECTED_RELEASE_VERSION}/", + } + ] + + def test_default_compose_does_not_provision_or_configure_redis(): compose = load_compose() services = compose["services"] diff --git a/tests/deploy/test_helm_deployment_k8s.py b/tests/deploy/test_helm_deployment_k8s.py index 9bc4ae447..e75a0882e 100644 --- a/tests/deploy/test_helm_deployment_k8s.py +++ b/tests/deploy/test_helm_deployment_k8s.py @@ -13,7 +13,10 @@ # See the License for the specific language governing permissions and # limitations under the License. +import re import subprocess +import tarfile +import tomllib from pathlib import Path from typing import Any @@ -21,6 +24,15 @@ REPO_ROOT = Path(__file__).resolve().parents[2] CHART_PATH = REPO_ROOT / "deploy" / "helm" / "deployment-k8s" +CHILD_CHART_DIR = REPO_ROOT / "deploy" / "helm" / "helm-charts-k8s" / "aiq" +CHILD_CHART_PATH = CHILD_CHART_DIR / "Chart.yaml" +PACKAGED_CHILD_CHART_PATH = CHART_PATH / "charts" / "aiq-0.0.5.tgz" +HELM_README_PATH = REPO_ROOT / "deploy" / "helm" / "README.md" +KUBERNETES_DOCS_PATH = REPO_ROOT / "docs" / "source" / "deployment" / "kubernetes.md" +OPENSEARCH_VALUES_PATH = REPO_ROOT / "deploy" / "helm" / "examples" / "aws-opensearch-serverless-values.yaml" +PYPROJECT_PATH = REPO_ROOT / "pyproject.toml" +EXPECTED_RELEASE_VERSION = "2.2.0" +EXPECTED_CHILD_CHART_VERSION = "0.0.5" def render_chart(*extra_args: str, namespace: str = "ns-aiq") -> list[dict[str, Any]]: @@ -43,6 +55,63 @@ def walk_values(value: Any): yield from walk_values(item) +def test_release_version_matches_helm_chart_images_and_docs(): + with PYPROJECT_PATH.open("rb") as pyproject_file: + package_version = tomllib.load(pyproject_file)["project"]["version"] + + parent_chart = yaml.safe_load((CHART_PATH / "Chart.yaml").read_text(encoding="utf-8")) + child_chart = yaml.safe_load(CHILD_CHART_PATH.read_text(encoding="utf-8")) + values = yaml.safe_load((CHART_PATH / "values.yaml").read_text(encoding="utf-8")) + opensearch_values = yaml.safe_load(OPENSEARCH_VALUES_PATH.read_text(encoding="utf-8")) + + with tarfile.open(PACKAGED_CHILD_CHART_PATH, "r:gz") as archive: + packaged_files = {} + for member in archive.getmembers(): + if not member.isfile(): + continue + packaged_file = archive.extractfile(member) + assert packaged_file is not None + relative_path = Path(member.name).relative_to("aiq").as_posix() + packaged_files[relative_path] = packaged_file.read() + + source_files = { + path.relative_to(CHILD_CHART_DIR).as_posix(): path.read_bytes() + for path in CHILD_CHART_DIR.rglob("*") + if path.is_file() + } + packaged_child_chart = yaml.safe_load(packaged_files["Chart.yaml"]) + + assert package_version == EXPECTED_RELEASE_VERSION + assert parent_chart["version"] == EXPECTED_RELEASE_VERSION + assert parent_chart["appVersion"] == EXPECTED_RELEASE_VERSION + assert child_chart["appVersion"] == EXPECTED_RELEASE_VERSION + assert child_chart["version"] == EXPECTED_CHILD_CHART_VERSION + assert packaged_child_chart == child_chart + assert packaged_files.keys() == source_files.keys() + assert {path: content for path, content in packaged_files.items() if path != "Chart.yaml"} == { + path: content for path, content in source_files.items() if path != "Chart.yaml" + } + assert parent_chart["dependencies"][0]["version"] == EXPECTED_CHILD_CHART_VERSION + assert values["aiq"]["apps"]["backend"]["image"]["tag"] == EXPECTED_RELEASE_VERSION + assert values["aiq"]["apps"]["frontend"]["image"]["tag"] == EXPECTED_RELEASE_VERSION + assert opensearch_values["aiq"]["apps"]["backend"]["image"]["tag"] == EXPECTED_RELEASE_VERSION + + rendered_images = { + manifest["metadata"]["name"]: manifest["spec"]["template"]["spec"]["containers"][0]["image"] + for manifest in render_chart() + if manifest.get("kind") == "Deployment" + } + assert rendered_images["aiq-backend"] == f"nvcr.io/nvidia/blueprint/aiq-agent:{EXPECTED_RELEASE_VERSION}" + assert rendered_images["aiq-frontend"] == f"nvcr.io/nvidia/blueprint/aiq-frontend:{EXPECTED_RELEASE_VERSION}" + + expected_chart_archive = f"aiq2-web-{EXPECTED_RELEASE_VERSION}.tgz" + chart_archive_pattern = re.compile(r"aiq2-web-[\w.-]+\.tgz") + for documentation_path in (HELM_README_PATH, KUBERNETES_DOCS_PATH): + documentation = documentation_path.read_text(encoding="utf-8") + assert set(chart_archive_pattern.findall(documentation)) == {expected_chart_archive} + assert f"**aiq2-web** version **{EXPECTED_RELEASE_VERSION}**" in documentation + + def test_default_chart_renders_referenced_configmaps_and_uses_user_supplied_secret(): manifests = render_chart() diff --git a/uv.lock b/uv.lock index c9aa59eea..d668adf48 100644 --- a/uv.lock +++ b/uv.lock @@ -195,7 +195,7 @@ wheels = [ [[package]] name = "aiq-agent" -version = "2.0.0" +version = "2.2.0" source = { editable = "." } dependencies = [ { name = "aiofiles" },