From 1b0bdc2feb398c220eedc7f0f47db965c94748c2 Mon Sep 17 00:00:00 2001 From: Piotr Mlocek Date: Fri, 6 Mar 2026 17:20:05 -0800 Subject: [PATCH 1/2] fix(build): propagate packaged version through cluster artifacts Closes #159 Pass the computed cargo version through Docker and cluster packaging paths so deployed clusters report the built artifact version while keeping latest and explicit version tags aligned. --- .github/workflows/docker-build.yml | 12 ++++++++++ .github/workflows/publish.yml | 13 +++++++++++ architecture/build-containers.md | 6 ++++- deploy/docker/Dockerfile.server | 4 ++++ deploy/docker/cluster-entrypoint.sh | 23 +++++++++++++++++-- .../kube/manifests/navigator-helmchart.yaml | 4 ++-- tasks/scripts/docker-build-cluster.sh | 10 +++++++- tasks/scripts/docker-build-component.sh | 9 ++++++++ tasks/scripts/docker-publish-multiarch.sh | 11 ++++++++- 9 files changed, 85 insertions(+), 7 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 800a7c927..c7d3ca46e 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -45,6 +45,17 @@ jobs: DOCKER_PUSH: ${{ inputs.push && '1' || '0' }} steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Fetch tags + run: git fetch --tags --force + + - name: Compute cargo version + id: version + run: | + set -euo pipefail + echo "cargo_version=$(uv run python tasks/scripts/release.py get-version --cargo)" >> "$GITHUB_OUTPUT" - name: Log in to GHCR run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin @@ -55,4 +66,5 @@ jobs: - name: Build ${{ inputs.component }} image env: DOCKER_BUILDER: navigator + NEMOCLAW_CARGO_VERSION: ${{ steps.version.outputs.cargo_version }} run: mise run --no-prepare docker:build:${{ inputs.component }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 55a99ed1e..288a61968 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -71,6 +71,17 @@ jobs: AWS_DEFAULT_REGION: us-west-2 steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Fetch tags + run: git fetch --tags --force + + - name: Compute cargo version + id: version + run: | + set -euo pipefail + echo "cargo_version=$(uv run python tasks/scripts/release.py get-version --cargo)" >> "$GITHUB_OUTPUT" - name: Log in to GHCR run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin @@ -86,6 +97,8 @@ jobs: DOCKER_BUILDER: navigator IMAGE_TAG: dev TAG_LATEST: "true" + EXTRA_DOCKER_TAGS: ${{ steps.version.outputs.cargo_version }} + NEMOCLAW_CARGO_VERSION: ${{ steps.version.outputs.cargo_version }} run: mise run --no-prepare docker:publish:cluster:multiarch build-python-wheels: diff --git a/architecture/build-containers.md b/architecture/build-containers.md index 1ef8dfc0c..46e25c5bb 100644 --- a/architecture/build-containers.md +++ b/architecture/build-containers.md @@ -451,12 +451,16 @@ Artifactory: - Wheels are uploaded to `s3://navigator-pypi-artifacts/navigator//`. - A follow-up job on the `nv` runner lists that version prefix, downloads the wheels, and publishes them to Artifactory. +- Container publish jobs compute the same Cargo version once and pass it through + Docker builds so `navigator-server` reports the packaged artifact version at runtime. +- Published images keep the floating `latest` tag and also receive an explicit + version tag for the same manifest. ### Auto-Deployed Components in Cluster When the cluster container starts, k3s automatically deploys these HelmChart CRs from `/var/lib/rancher/k3s/server/manifests/`: -1. **NemoClaw** (from `navigator-0.1.0.tgz` in the static charts directory) -- deployed into `navigator` namespace. The HelmChart CR's `valuesContent` configures image references, SSH gateway settings, and TLS options. These values are rewritten by the entrypoint script based on environment variables from the bootstrap code. +1. **NemoClaw** (from the packaged `navigator-.tgz` copied into the static charts directory) -- deployed into `navigator` namespace. The HelmChart CR's `valuesContent` configures image references, SSH gateway settings, and TLS options. The entrypoint script rewrites the chart filename placeholder, injects the chart checksum, and patches runtime image references from environment variables provided by the bootstrap code. ## Implementation References diff --git a/deploy/docker/Dockerfile.server b/deploy/docker/Dockerfile.server index e387fe75e..e31af5305 100644 --- a/deploy/docker/Dockerfile.server +++ b/deploy/docker/Dockerfile.server @@ -10,6 +10,7 @@ FROM --platform=$BUILDPLATFORM rust:1.88-slim AS builder ARG TARGETARCH ARG BUILDARCH +ARG NEMOCLAW_CARGO_VERSION ARG CARGO_TARGET_CACHE_SCOPE=default # Install build dependencies @@ -71,6 +72,9 @@ RUN --mount=type=cache,id=cargo-registry-server-${TARGETARCH},sharing=locked,tar --mount=type=cache,id=cargo-target-server-${TARGETARCH}-${CARGO_TARGET_CACHE_SCOPE},sharing=locked,target=/build/target \ --mount=type=cache,id=sccache-server-${TARGETARCH},sharing=locked,target=/tmp/sccache \ . cross-build.sh && \ + if [ -n "${NEMOCLAW_CARGO_VERSION:-}" ]; then \ + sed -i -E '/^\[workspace\.package\]/,/^\[/{s/^version[[:space:]]*=[[:space:]]*".*"/version = "'"${NEMOCLAW_CARGO_VERSION}"'"/}' Cargo.toml; \ + fi && \ cargo_cross_build --release -p navigator-server && \ cp "$(cross_output_dir release)/navigator-server" /build/navigator-server diff --git a/deploy/docker/cluster-entrypoint.sh b/deploy/docker/cluster-entrypoint.sh index 9cca0ff0e..c10fea87a 100644 --- a/deploy/docker/cluster-entrypoint.sh +++ b/deploy/docker/cluster-entrypoint.sh @@ -135,6 +135,8 @@ fi K3S_CHARTS="/var/lib/rancher/k3s/server/static/charts" BUNDLED_CHARTS="/opt/navigator/charts" CHART_CHECKSUM="" +NAV_CHART_FILENAME="" +NAV_CHART_COUNT=0 if [ -d "$BUNDLED_CHARTS" ]; then echo "Copying bundled charts to k3s..." @@ -146,13 +148,25 @@ if [ -d "$BUNDLED_CHARTS" ]; then # HelmChart manifest below. When the chart content changes between image # versions the checksum changes, which modifies the HelmChart CR spec and # forces the k3s Helm controller to re-install. - NAV_CHART="$BUNDLED_CHARTS/navigator-0.1.0.tgz" - if [ -f "$NAV_CHART" ]; then + for candidate in "$BUNDLED_CHARTS"/navigator-*.tgz; do + [ ! -f "$candidate" ] && continue + NAV_CHART="$candidate" + NAV_CHART_COUNT=$((NAV_CHART_COUNT + 1)) + done + + if [ "$NAV_CHART_COUNT" -eq 1 ] && [ -n "$NAV_CHART" ]; then + NAV_CHART_FILENAME=$(basename "$NAV_CHART") if command -v sha256sum >/dev/null 2>&1; then CHART_CHECKSUM=$(sha256sum "$NAV_CHART" | cut -d ' ' -f 1) elif command -v shasum >/dev/null 2>&1; then CHART_CHECKSUM=$(shasum -a 256 "$NAV_CHART" | cut -d ' ' -f 1) fi + elif [ "$NAV_CHART_COUNT" -eq 0 ]; then + echo "Error: bundled navigator chart not found in ${BUNDLED_CHARTS}" >&2 + exit 1 + else + echo "Error: expected exactly one bundled navigator chart, found ${NAV_CHART_COUNT}" >&2 + exit 1 fi fi @@ -249,6 +263,11 @@ if [ -n "${IMAGE_PULL_POLICY:-}" ] && [ -f "$HELMCHART" ]; then sed -i "s|pullPolicy: Always|pullPolicy: ${IMAGE_PULL_POLICY}|" "$HELMCHART" fi +if [ -n "$NAV_CHART_FILENAME" ] && [ -f "$HELMCHART" ]; then + echo "Setting navigator chart filename: ${NAV_CHART_FILENAME}" + sed -i "s|__NAVIGATOR_CHART_FILENAME__|${NAV_CHART_FILENAME}|g" "$HELMCHART" +fi + # Generate a random SSH handshake secret for the NSSH1 HMAC handshake between # the gateway and sandbox SSH servers. This is required — the server will refuse # to start without it. diff --git a/deploy/kube/manifests/navigator-helmchart.yaml b/deploy/kube/manifests/navigator-helmchart.yaml index d99eeccd1..33f583aea 100644 --- a/deploy/kube/manifests/navigator-helmchart.yaml +++ b/deploy/kube/manifests/navigator-helmchart.yaml @@ -6,7 +6,7 @@ # automatically installed when the k3s cluster starts. # # The chart tarball is served from the k3s static file server at: -# https:///static/charts/navigator-0.1.0.tgz +# https:///static/charts/__NAVIGATOR_CHART_FILENAME__ # # Component images are pulled at runtime from the distribution registry. # Authentication is configured via /etc/rancher/k3s/registries.yaml, @@ -18,7 +18,7 @@ metadata: name: navigator namespace: kube-system spec: - chart: https://%{KUBERNETES_API}%/static/charts/navigator-0.1.0.tgz + chart: https://%{KUBERNETES_API}%/static/charts/__NAVIGATOR_CHART_FILENAME__ targetNamespace: navigator createNamespace: true valuesContent: |- diff --git a/tasks/scripts/docker-build-cluster.sh b/tasks/scripts/docker-build-cluster.sh index 1d4679187..0b12df856 100755 --- a/tasks/scripts/docker-build-cluster.sh +++ b/tasks/scripts/docker-build-cluster.sh @@ -16,6 +16,10 @@ set -euo pipefail IMAGE_TAG=${IMAGE_TAG:-dev} +CARGO_VERSION=${NEMOCLAW_CARGO_VERSION:-} +if [[ -z "${CARGO_VERSION}" ]]; then + CARGO_VERSION=$(uv run python tasks/scripts/release.py get-version --cargo) +fi IMAGE_NAME="navigator/cluster" if [[ -n "${IMAGE_REGISTRY:-}" ]]; then IMAGE_NAME="${IMAGE_REGISTRY}/cluster" @@ -48,10 +52,14 @@ fi # Create build directory for charts mkdir -p deploy/docker/.build/charts +rm -f deploy/docker/.build/charts/navigator-*.tgz # Package navigator helm chart echo "Packaging navigator helm chart..." -helm package deploy/helm/navigator -d deploy/docker/.build/charts/ +helm package deploy/helm/navigator \ + --version "${CARGO_VERSION}" \ + --app-version "${CARGO_VERSION}" \ + -d deploy/docker/.build/charts/ # Build cluster image (no bundled component images — they are pulled at runtime # from the distribution registry; credentials are injected at deploy time) diff --git a/tasks/scripts/docker-build-component.sh b/tasks/scripts/docker-build-component.sh index 8516967c0..5da3f4276 100755 --- a/tasks/scripts/docker-build-component.sh +++ b/tasks/scripts/docker-build-component.sh @@ -135,6 +135,14 @@ if [[ -n "${SCCACHE_MEMCACHED_ENDPOINT:-}" ]]; then SCCACHE_ARGS=(--build-arg "SCCACHE_MEMCACHED_ENDPOINT=${SCCACHE_MEMCACHED_ENDPOINT}") fi +VERSION_ARGS=() +if [[ -n "${NEMOCLAW_CARGO_VERSION:-}" ]]; then + VERSION_ARGS=(--build-arg "NEMOCLAW_CARGO_VERSION=${NEMOCLAW_CARGO_VERSION}") +elif [[ "${COMPONENT}" == "server" ]]; then + CARGO_VERSION=$(uv run python tasks/scripts/release.py get-version --cargo) + VERSION_ARGS=(--build-arg "NEMOCLAW_CARGO_VERSION=${CARGO_VERSION}") +fi + LOCK_HASH=$(sha256_16 Cargo.lock) RUST_SCOPE=${RUST_TOOLCHAIN_SCOPE:-$(detect_rust_scope "${DOCKERFILE}")} CACHE_SCOPE_INPUT="v1|${COMPONENT}|${VARIANT:-base}|${LOCK_HASH}|${RUST_SCOPE}" @@ -145,6 +153,7 @@ docker buildx build \ ${DOCKER_PLATFORM:+--platform ${DOCKER_PLATFORM}} \ ${CACHE_ARGS[@]+"${CACHE_ARGS[@]}"} \ ${SCCACHE_ARGS[@]+"${SCCACHE_ARGS[@]}"} \ + ${VERSION_ARGS[@]+"${VERSION_ARGS[@]}"} \ --build-arg "CARGO_TARGET_CACHE_SCOPE=${CARGO_TARGET_CACHE_SCOPE}" \ -f "${DOCKERFILE}" \ -t "${IMAGE_NAME}:${IMAGE_TAG}" \ diff --git a/tasks/scripts/docker-publish-multiarch.sh b/tasks/scripts/docker-publish-multiarch.sh index ef775dc58..ba39390c0 100755 --- a/tasks/scripts/docker-publish-multiarch.sh +++ b/tasks/scripts/docker-publish-multiarch.sh @@ -80,6 +80,10 @@ fi # --------------------------------------------------------------------------- IMAGE_TAG=${IMAGE_TAG:-dev} PLATFORMS=${DOCKER_PLATFORMS:-linux/amd64,linux/arm64} +CARGO_VERSION=${NEMOCLAW_CARGO_VERSION:-} +if [[ -z "${CARGO_VERSION}" ]]; then + CARGO_VERSION=$(uv run python tasks/scripts/release.py get-version --cargo) +fi EXTRA_BUILD_FLAGS="" TAG_LATEST=${TAG_LATEST:-false} EXTRA_DOCKER_TAGS_RAW=${EXTRA_DOCKER_TAGS:-} @@ -159,6 +163,7 @@ for component in sandbox server; do if [ "$component" = "sandbox" ]; then BUILD_ARGS="--build-arg RUST_BUILD_PROFILE=${RUST_BUILD_PROFILE:-release}" fi + BUILD_ARGS="${BUILD_ARGS} --build-arg NEMOCLAW_CARGO_VERSION=${CARGO_VERSION}" if [ -n "${SCCACHE_MEMCACHED_ENDPOINT:-}" ]; then BUILD_ARGS="${BUILD_ARGS} --build-arg SCCACHE_MEMCACHED_ENDPOINT=${SCCACHE_MEMCACHED_ENDPOINT}" fi @@ -182,8 +187,12 @@ done # Step 2: Package helm charts (architecture-independent) # --------------------------------------------------------------------------- mkdir -p deploy/docker/.build/charts +rm -f deploy/docker/.build/charts/navigator-*.tgz echo "Packaging navigator helm chart..." -helm package deploy/helm/navigator -d deploy/docker/.build/charts/ +helm package deploy/helm/navigator \ + --version "${CARGO_VERSION}" \ + --app-version "${CARGO_VERSION}" \ + -d deploy/docker/.build/charts/ # --------------------------------------------------------------------------- # Step 3: Build and push multi-arch cluster image. From 0a0d0ebac239394c3bef36917386edf57c2a2768 Mon Sep 17 00:00:00 2001 From: Piotr Mlocek Date: Fri, 6 Mar 2026 17:24:55 -0800 Subject: [PATCH 2/2] refactor(build): drop chart version propagation Keep the runtime fix focused on Docker/server artifact versions and leave the bundled Helm chart path/version unchanged. --- architecture/build-containers.md | 2 +- deploy/docker/cluster-entrypoint.sh | 23 ++----------------- .../kube/manifests/navigator-helmchart.yaml | 4 ++-- tasks/scripts/docker-build-cluster.sh | 10 +------- tasks/scripts/docker-publish-multiarch.sh | 6 +---- 5 files changed, 7 insertions(+), 38 deletions(-) diff --git a/architecture/build-containers.md b/architecture/build-containers.md index 46e25c5bb..b3526c083 100644 --- a/architecture/build-containers.md +++ b/architecture/build-containers.md @@ -460,7 +460,7 @@ Artifactory: When the cluster container starts, k3s automatically deploys these HelmChart CRs from `/var/lib/rancher/k3s/server/manifests/`: -1. **NemoClaw** (from the packaged `navigator-.tgz` copied into the static charts directory) -- deployed into `navigator` namespace. The HelmChart CR's `valuesContent` configures image references, SSH gateway settings, and TLS options. The entrypoint script rewrites the chart filename placeholder, injects the chart checksum, and patches runtime image references from environment variables provided by the bootstrap code. +1. **NemoClaw** (from `navigator-0.1.0.tgz` in the static charts directory) -- deployed into `navigator` namespace. The HelmChart CR's `valuesContent` configures image references, SSH gateway settings, and TLS options. These values are rewritten by the entrypoint script based on environment variables from the bootstrap code. ## Implementation References diff --git a/deploy/docker/cluster-entrypoint.sh b/deploy/docker/cluster-entrypoint.sh index c10fea87a..9cca0ff0e 100644 --- a/deploy/docker/cluster-entrypoint.sh +++ b/deploy/docker/cluster-entrypoint.sh @@ -135,8 +135,6 @@ fi K3S_CHARTS="/var/lib/rancher/k3s/server/static/charts" BUNDLED_CHARTS="/opt/navigator/charts" CHART_CHECKSUM="" -NAV_CHART_FILENAME="" -NAV_CHART_COUNT=0 if [ -d "$BUNDLED_CHARTS" ]; then echo "Copying bundled charts to k3s..." @@ -148,25 +146,13 @@ if [ -d "$BUNDLED_CHARTS" ]; then # HelmChart manifest below. When the chart content changes between image # versions the checksum changes, which modifies the HelmChart CR spec and # forces the k3s Helm controller to re-install. - for candidate in "$BUNDLED_CHARTS"/navigator-*.tgz; do - [ ! -f "$candidate" ] && continue - NAV_CHART="$candidate" - NAV_CHART_COUNT=$((NAV_CHART_COUNT + 1)) - done - - if [ "$NAV_CHART_COUNT" -eq 1 ] && [ -n "$NAV_CHART" ]; then - NAV_CHART_FILENAME=$(basename "$NAV_CHART") + NAV_CHART="$BUNDLED_CHARTS/navigator-0.1.0.tgz" + if [ -f "$NAV_CHART" ]; then if command -v sha256sum >/dev/null 2>&1; then CHART_CHECKSUM=$(sha256sum "$NAV_CHART" | cut -d ' ' -f 1) elif command -v shasum >/dev/null 2>&1; then CHART_CHECKSUM=$(shasum -a 256 "$NAV_CHART" | cut -d ' ' -f 1) fi - elif [ "$NAV_CHART_COUNT" -eq 0 ]; then - echo "Error: bundled navigator chart not found in ${BUNDLED_CHARTS}" >&2 - exit 1 - else - echo "Error: expected exactly one bundled navigator chart, found ${NAV_CHART_COUNT}" >&2 - exit 1 fi fi @@ -263,11 +249,6 @@ if [ -n "${IMAGE_PULL_POLICY:-}" ] && [ -f "$HELMCHART" ]; then sed -i "s|pullPolicy: Always|pullPolicy: ${IMAGE_PULL_POLICY}|" "$HELMCHART" fi -if [ -n "$NAV_CHART_FILENAME" ] && [ -f "$HELMCHART" ]; then - echo "Setting navigator chart filename: ${NAV_CHART_FILENAME}" - sed -i "s|__NAVIGATOR_CHART_FILENAME__|${NAV_CHART_FILENAME}|g" "$HELMCHART" -fi - # Generate a random SSH handshake secret for the NSSH1 HMAC handshake between # the gateway and sandbox SSH servers. This is required — the server will refuse # to start without it. diff --git a/deploy/kube/manifests/navigator-helmchart.yaml b/deploy/kube/manifests/navigator-helmchart.yaml index 33f583aea..d99eeccd1 100644 --- a/deploy/kube/manifests/navigator-helmchart.yaml +++ b/deploy/kube/manifests/navigator-helmchart.yaml @@ -6,7 +6,7 @@ # automatically installed when the k3s cluster starts. # # The chart tarball is served from the k3s static file server at: -# https:///static/charts/__NAVIGATOR_CHART_FILENAME__ +# https:///static/charts/navigator-0.1.0.tgz # # Component images are pulled at runtime from the distribution registry. # Authentication is configured via /etc/rancher/k3s/registries.yaml, @@ -18,7 +18,7 @@ metadata: name: navigator namespace: kube-system spec: - chart: https://%{KUBERNETES_API}%/static/charts/__NAVIGATOR_CHART_FILENAME__ + chart: https://%{KUBERNETES_API}%/static/charts/navigator-0.1.0.tgz targetNamespace: navigator createNamespace: true valuesContent: |- diff --git a/tasks/scripts/docker-build-cluster.sh b/tasks/scripts/docker-build-cluster.sh index 0b12df856..1d4679187 100755 --- a/tasks/scripts/docker-build-cluster.sh +++ b/tasks/scripts/docker-build-cluster.sh @@ -16,10 +16,6 @@ set -euo pipefail IMAGE_TAG=${IMAGE_TAG:-dev} -CARGO_VERSION=${NEMOCLAW_CARGO_VERSION:-} -if [[ -z "${CARGO_VERSION}" ]]; then - CARGO_VERSION=$(uv run python tasks/scripts/release.py get-version --cargo) -fi IMAGE_NAME="navigator/cluster" if [[ -n "${IMAGE_REGISTRY:-}" ]]; then IMAGE_NAME="${IMAGE_REGISTRY}/cluster" @@ -52,14 +48,10 @@ fi # Create build directory for charts mkdir -p deploy/docker/.build/charts -rm -f deploy/docker/.build/charts/navigator-*.tgz # Package navigator helm chart echo "Packaging navigator helm chart..." -helm package deploy/helm/navigator \ - --version "${CARGO_VERSION}" \ - --app-version "${CARGO_VERSION}" \ - -d deploy/docker/.build/charts/ +helm package deploy/helm/navigator -d deploy/docker/.build/charts/ # Build cluster image (no bundled component images — they are pulled at runtime # from the distribution registry; credentials are injected at deploy time) diff --git a/tasks/scripts/docker-publish-multiarch.sh b/tasks/scripts/docker-publish-multiarch.sh index ba39390c0..1c3bd9a1f 100755 --- a/tasks/scripts/docker-publish-multiarch.sh +++ b/tasks/scripts/docker-publish-multiarch.sh @@ -187,12 +187,8 @@ done # Step 2: Package helm charts (architecture-independent) # --------------------------------------------------------------------------- mkdir -p deploy/docker/.build/charts -rm -f deploy/docker/.build/charts/navigator-*.tgz echo "Packaging navigator helm chart..." -helm package deploy/helm/navigator \ - --version "${CARGO_VERSION}" \ - --app-version "${CARGO_VERSION}" \ - -d deploy/docker/.build/charts/ +helm package deploy/helm/navigator -d deploy/docker/.build/charts/ # --------------------------------------------------------------------------- # Step 3: Build and push multi-arch cluster image.