diff --git a/.github/workflows/publish_rust.yml b/.github/workflows/publish_rust.yml index 676b433d5..651f1e18a 100644 --- a/.github/workflows/publish_rust.yml +++ b/.github/workflows/publish_rust.yml @@ -63,7 +63,7 @@ jobs: run: | set -euo pipefail - just set-version "$RELEASE_TAG" + just set-cargo-version "$RELEASE_TAG" version="$(sed -n 's/^version = "\(.*\)"$/\1/p' Cargo.toml | head -n 1)" packages=( nemo-fabric-core @@ -76,5 +76,19 @@ jobs: continue fi - cargo publish --package "$package" --no-verify --allow-dirty + cargo publish --package "$package" --no-verify --locked --allow-dirty + + if [[ "$package" == "nemo-fabric-core" ]]; then + for attempt in {1..12}; do + if cargo info "${package}@${version}" --registry crates-io >/dev/null 2>&1; then + break + fi + if [[ "$attempt" == 12 ]]; then + echo "${package} ${version} did not become visible on crates.io" >&2 + exit 1 + fi + echo "Waiting for ${package} ${version} to become visible on crates.io" + sleep 10 + done + fi done diff --git a/.github/workflows/publish_typescript.yml b/.github/workflows/publish_typescript.yml index a7c369d6e..9cab9d6ae 100644 --- a/.github/workflows/publish_typescript.yml +++ b/.github/workflows/publish_typescript.yml @@ -94,9 +94,6 @@ jobs: esac python3 scripts/ci/set_typescript_project_version.py "$version" - git diff --exit-code -- \ - adapter-contract/typescript/package.json \ - adapter-contract/typescript/package-lock.json { echo "version=$version" echo "dist_tag=$dist_tag" diff --git a/RELEASING.md b/RELEASING.md index 1a67b8c1c..1a7e815aa 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -131,19 +131,26 @@ Before you create a release tag, confirm the following: 1. The intended release commit is already on the release branch you intend to tag. For frozen release lines, tag the matching `release/*` branch. -2. The release commit contains the final version bump, docs updates, and any - public API changes that belong in the release. +2. The release commit contains the final stable base version, docs updates, and + any public API changes that belong in the release. Prerelease versions are + derived from beta or RC tags in disposable workflow checkouts. 3. The working tree you use for local validation is clean or disposable. ## Prepare The Release Commit -Update the versioned source files in the release PR or release-prep commit. -Prefer the repository helper: +Update the versioned source files in the release PR or release-prep commit to +the stable base version for the release line. Prefer the repository helper: ```bash -just set-version +just set-version +# For example: just set-version 0.1.0 ``` +For beta and RC tags, keep the committed source metadata at the corresponding +stable base version. The tag workflows normalize the prerelease tag and stamp +ecosystem-specific package metadata in their disposable checkouts. Do not +commit RC-specific versions or internal dependency pins to the release branch. + The helper updates: 1. The root [`Cargo.toml`](Cargo.toml) workspace version. @@ -258,7 +265,8 @@ older release line. ## Cut An RC Tag After the release commit is merged and validated, create and push a signed, -annotated tag. Set the complete release-candidate version: +annotated tag. Set the complete release-candidate version; the release branch +continues to carry its matching stable base version: ```bash export RELEASE_VERSION=0.1.0-rc.1 @@ -275,6 +283,9 @@ RELEASE_SHA="$(git rev-parse HEAD)" REMOTE_RELEASE_SHA="$(git rev-parse "upstream/${RELEASE_BRANCH}^{commit}")" test "${RELEASE_SHA}" = "${REMOTE_RELEASE_SHA}" test "$(just normalize-release-tag "${RELEASE_TAG}")" = "${RELEASE_VERSION}" +BASE_RELEASE_VERSION="${RELEASE_VERSION%%-*}" +CURRENT_VERSION="$(sed -n 's/^version = "\(.*\)"$/\1/p' Cargo.toml | head -n 1)" +test "${CURRENT_VERSION}" = "${BASE_RELEASE_VERSION}" if git ls-remote --exit-code --tags upstream "refs/tags/${RELEASE_TAG}" >/dev/null; then echo "Error: remote tag ${RELEASE_TAG} already exists" >&2 @@ -393,8 +404,8 @@ Pushing a valid tag triggers : The release pipeline then: -1. Validates the tag format with `just set-version` or - `normalize_release_tag.py`. +1. Normalizes and validates the tag format, then stamps ecosystem-specific + package metadata in each disposable workflow checkout. 2. Builds platform `nemo-fabric-runtime` wheels and pure-Python `nemo-fabric` and adapter wheels with the exact tag version, then uploads them as GitHub Actions artifacts. diff --git a/justfile b/justfile index 55fcf0a97..ffdbf0ca7 100644 --- a/justfile +++ b/justfile @@ -384,15 +384,32 @@ lock-python: # Normalize a release tag to the version used by package metadata. normalize-release-tag tag: - @uv run --no-project --no-cache python scripts/ci/normalize_release_tag.py "{{ tag }}" + @uv run --no-project --no-cache python scripts/ci/normalize_release_tag.py {{ quote(tag) }} + +# Apply a release version only to Cargo workspace metadata and Cargo.lock. +# Tag publication uses this narrow recipe in a disposable checkout. +set-cargo-version version="": + #!/usr/bin/env bash + {{ bash_helpers }} + version={{ quote(version) }} + if [[ -z "$version" ]]; then + version={{ quote(ref_name) }} + fi + if [[ -z "$version" ]]; then + echo "Error: version is required for set-cargo-version" >&2 + exit 1 + fi + version="$(just normalize-release-tag "$version")" + cd "$REPO_ROOT" + set_cargo_workspace_version "$version" # [version-or-tag] or --set ref_name= set-version version="": #!/usr/bin/env bash {{ bash_helpers }} - version="{{ version }}" + version={{ quote(version) }} if [[ -z "$version" ]]; then - version="{{ ref_name }}" + version={{ quote(ref_name) }} fi if [[ -z "$version" ]]; then echo "Error: version is required for set-version" >&2 diff --git a/tests/scripts/test_justfile_release_tag_quoting.py b/tests/scripts/test_justfile_release_tag_quoting.py new file mode 100644 index 000000000..3ed0d006e --- /dev/null +++ b/tests/scripts/test_justfile_release_tag_quoting.py @@ -0,0 +1,51 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import os +import subprocess +from pathlib import Path + +import pytest + + +REPO_ROOT = Path(__file__).resolve().parents[2] + + +@pytest.mark.parametrize( + "arguments", + [ + ("normalize-release-tag",), + ("set-cargo-version",), + ("set-version",), + ("--set", "ref_name={payload}", "set-cargo-version"), + ("--set", "ref_name={payload}", "set-version"), + ], +) +def test_release_tag_interpolation_does_not_execute_command_substitution( + tmp_path: Path, + arguments: tuple[str, ...], +): + marker = tmp_path / "interpolation-executed" + payload = f"$(touch {marker})" + command = [ + "just", + *(argument.format(payload=payload) for argument in arguments), + ] + if not any("{payload}" in argument for argument in arguments): + command.append(payload) + + environment = os.environ.copy() + environment["UV_CACHE_DIR"] = str(tmp_path / "uv-cache") + result = subprocess.run( + command, + cwd=REPO_ROOT, + env=environment, + capture_output=True, + text=True, + check=False, + ) + + assert result.returncode != 0 + assert not marker.exists()