From 6db7ede67c0b7abd3e92ab0f24b9106648f7cb00 Mon Sep 17 00:00:00 2001 From: Matt Kornfield Date: Wed, 15 Jul 2026 13:55:45 +0000 Subject: [PATCH 1/2] chore: make helm versioning follow repo version * Also allow for an override case Signed-off-by: Matt Kornfield --- .github/workflows/release.yaml | 55 ++++++++++++++++++++++++++++++---- RELEASING.md | 11 ++++--- 2 files changed, 57 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index d9a6ea19c9c..f611232d7fc 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -60,6 +60,11 @@ on: required: false type: boolean default: false + helm-version: + description: "Optional exact Helm chart version override for stable Helm-only releases." + required: false + type: string + default: "" update-ngc-metadata: description: "Synchronize NGC metadata from the this branch." required: false @@ -148,6 +153,7 @@ jobs: has_wheels: ${{ steps.plan.outputs.has_wheels }} has_containers: ${{ steps.plan.outputs.has_containers }} include_helm: ${{ steps.plan.outputs.include_helm }} + helm_version_override: ${{ steps.plan.outputs.helm_version_override }} update_ngc_metadata: ${{ steps.plan.outputs.update_ngc_metadata }} send_notifications: ${{ steps.plan.outputs.send_notifications }} dry_run: ${{ steps.plan.outputs.dry_run }} @@ -173,6 +179,7 @@ jobs: const updateNgcMetadata = isManual && inputs["update-ngc-metadata"] === "true"; const sendNotifications = inputs["send-notifications"] !== "false"; const dryRun = isManual && inputs["dry-run"] === "true"; + const helmVersionOverride = isManual ? (inputs["helm-version"] ?? "").trim() : ""; let sourceSha = isManual ? (inputs["source-sha"] ?? "").trim() : context.sha; const version = releaseType === "stable" ? (inputs.version ?? "").trim() : ""; @@ -271,6 +278,20 @@ jobs: const wheelIds = wheels.map((wheel) => wheel.id); const containerIds = containers.map((container) => container.id); + if (helmVersionOverride) { + if ( + releaseType !== "stable" + || !includeHelm + || wheelIds.length > 0 + || containerIds.length > 0 + ) { + throw new Error("helm-version can only be used for stable Helm-only releases."); + } + if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(helmVersionOverride)) { + throw new Error("helm-version must be a SemVer chart version."); + } + } + const nightlyTimestamp = releaseType === "nightly" ? new Date().toISOString().replace(/\D/g, "").slice(0, 14) : ""; @@ -291,6 +312,7 @@ jobs: core.setOutput("has_wheels", String(wheelIds.length > 0)); core.setOutput("has_containers", String(containerIds.length > 0)); core.setOutput("include_helm", String(includeHelm)); + core.setOutput("helm_version_override", helmVersionOverride); core.setOutput("update_ngc_metadata", String(updateNgcMetadata)); core.setOutput("send_notifications", String(sendNotifications)); core.setOutput("dry_run", String(dryRun)); @@ -643,6 +665,8 @@ jobs: uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ needs.plan-release.outputs.source_sha }} + fetch-depth: 0 + fetch-tags: true persist-credentials: false - name: Set up Helm @@ -669,6 +693,7 @@ jobs: RELEASE_VERSION: ${{ needs.plan-release.outputs.version }} RELEASE_LABEL: ${{ needs.plan-release.outputs.release_label }} NIGHTLY_TIMESTAMP: ${{ needs.plan-release.outputs.nightly_timestamp }} + HELM_VERSION_OVERRIDE: ${{ needs.plan-release.outputs.helm_version_override }} run: | set -euo pipefail @@ -680,17 +705,37 @@ jobs: helm repo add nvidia https://helm.ngc.nvidia.com/nvidia helm dependency build "${chart_dir}" - chart_version="$(yq -r '.version' "${chart_dir}/Chart.yaml")" if [[ "${RELEASE_TYPE}" == "nightly" ]]; then - chart_version="${chart_version}-nightly-${NIGHTLY_TIMESTAMP}" release_registry="${NIGHTLY_IMAGE_REGISTRY}" else - # TODO: Decide whether stable chart versions should track the platform - # release version or remain independently managed in Chart.yaml. - chart_version="${RELEASE_VERSION}" release_registry="${STABLE_IMAGE_REGISTRY}" fi + chart_version="${HELM_VERSION_OVERRIDE}" + if [[ -z "${chart_version}" ]]; then + if [[ "${RELEASE_TYPE}" == "nightly" ]]; then + # Nightlies align to the release core from the latest release or + # RC tag. Build-metadata chart fixes use helm-version instead. + base_version="$( + git tag --merged HEAD --list \ + | awk ' + /^[0-9]+\.[0-9]+\.[0-9]+(-rc[0-9]+)?$/ { + sub(/-rc[0-9]+$/, "") + print + } + ' \ + | sort -V -u \ + | tail -n 1 + )" + if [[ -z "${base_version}" ]]; then + base_version="$(yq -r '.version' "${chart_dir}/Chart.yaml")" + fi + chart_version="${base_version}-nightly-${NIGHTLY_TIMESTAMP}" + else + chart_version="${RELEASE_VERSION}" + fi + fi + yq -i ".platformConfig.platform.image_registry = \"${release_registry}\"" "${chart_dir}/values.yaml" yq -i ".api.image.repository = \"${release_registry}/nmp-api\"" "${chart_dir}/values.yaml" yq -i ".core.image.repository = \"${release_registry}/nmp-api\"" "${chart_dir}/values.yaml" diff --git a/RELEASING.md b/RELEASING.md index 060d959335a..24d75f22862 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -56,6 +56,7 @@ and select **Run workflow**. The form shows the allowed custom artifact IDs. | `release-scope` | `all` by default. Select `wheels`, `containers`, `helm`, or `custom` for a subset. | | `wheel-ids`, `container-ids` | Comma-separated IDs used only with `release-scope: custom`. Each ID must be in the catalog above; duplicates and empty entries fail validation. | | `include-helm` | Includes the Helm chart in a custom release. | +| `helm-version` | Optional exact SemVer Helm chart version for stable Helm-only releases. The stable release label still comes from `version`. | | `update-ngc-metadata` | Also runs the reusable NGC metadata workflow for `nemo-platform` and `nemo-platform-dev`. It checks out the workflow ref, normally `main`. | | `send-notifications` | Sends Slack start and final-status notifications. Defaults to `true`. | | `dry-run` | Validates the selected source and packages the selected Helm chart, but does not publish, dispatch external work, poll, create a GitHub release, or signal deployment. The start notification intentionally still runs when notifications are enabled. | @@ -68,6 +69,7 @@ Examples: | Stable full release | `release-type: stable`, `source-sha: <40-character SHA>`, `version: `, `release-scope: all`. | | One container | `release-scope: custom`, `container-ids: nmp-automodel-tasks`. | | Helm-only validation | `release-scope: helm`, `dry-run: true`. | +| Stable Helm-only chart override | `release-type: stable`, `source-sha: <40-character SHA>`, `version: 0.1.0`, `release-scope: helm`, `helm-version: 0.1.0+helmfix1`. | Nightlies also run automatically Monday through Friday at 8:00 PM America/Los_Angeles. @@ -84,10 +86,11 @@ America/Los_Angeles. 4. Dispatches wheel, container, and stable-release registration work to the configured internal release repository. The selected source SHA, release type, version, and selected IDs are passed with the dispatch. -5. Packages the Helm chart. A nightly chart uses the `Chart.yaml` version with - `-nightly-` appended. A stable chart currently uses the - stable release version. Whether stable chart versions should instead remain - independently managed in `Chart.yaml` is an open policy decision. +5. Packages the Helm chart. A nightly chart uses the latest release or RC Git + tag core reachable from the selected source with `-nightly-` + appended, falling back to the `Chart.yaml` version if no such tag exists. A + stable chart uses the stable release version unless `helm-version` is set for + a stable Helm-only release. 6. Waits for every selected final artifact to become public before continuing. The polling job times out after four hours and sends a Slack alert after two hours if it is still waiting. From 128d98adc54359d89b6c59a49d190d8cd7edab5c Mon Sep 17 00:00:00 2001 From: Matt Kornfield Date: Wed, 15 Jul 2026 18:16:03 +0000 Subject: [PATCH 2/2] chore: move helm version determinant to plan Signed-off-by: Matt Kornfield --- .github/workflows/release.yaml | 96 +++++++++++++++++++++++----------- RELEASING.md | 18 +++---- 2 files changed, 74 insertions(+), 40 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index f611232d7fc..8ed1b614d79 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -148,6 +148,7 @@ jobs: release_label: ${{ steps.plan.outputs.release_label }} nightly_timestamp: ${{ steps.plan.outputs.nightly_timestamp }} wheel_version: ${{ steps.wheel-version.outputs.wheel_version }} + helm_version: ${{ steps.helm-version.outputs.helm_version }} wheel_ids: ${{ steps.plan.outputs.wheel_ids }} container_ids: ${{ steps.plan.outputs.container_ids }} has_wheels: ${{ steps.plan.outputs.has_wheels }} @@ -172,6 +173,14 @@ jobs: const allContainers = JSON.parse(process.env.RELEASE_CONTAINERS_JSON); const allWheelIds = allWheels.map((wheel) => wheel.id); const allContainerIds = allContainers.map((container) => container.id); + const semverCorePattern = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/; + // ECMA-compatible SemVer 2.0.0 pattern from https://semver.org/. + const semverPattern = new RegExp( + "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)" + + "(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)" + + "(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?" + + "(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$", + ); const inputs = context.payload.inputs ?? {}; const isManual = context.eventName === "workflow_dispatch"; const releaseType = isManual ? inputs["release-type"] : "nightly"; @@ -188,7 +197,7 @@ jobs: if (!/^[0-9a-f]{40}$/i.test(sourceSha)) { throw new Error("Stable releases require an exact 40-character source SHA."); } - if (!/^\d+\.\d+\.\d+$/.test(version)) { + if (!semverCorePattern.test(version)) { throw new Error("Stable releases require a MAJOR.MINOR.PATCH version."); } } else { @@ -287,7 +296,7 @@ jobs: ) { throw new Error("helm-version can only be used for stable Helm-only releases."); } - if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(helmVersionOverride)) { + if (!semverPattern.test(helmVersionOverride)) { throw new Error("helm-version must be a SemVer chart version."); } } @@ -469,6 +478,58 @@ jobs: wheel_version="$(python3 .github/scripts/stamp_sdk_version.py "${args[@]}")" echo "wheel_version=${wheel_version}" >> "${GITHUB_OUTPUT}" + - name: Resolve Helm version + id: helm-version + if: steps.plan.outputs.include_helm == 'true' + shell: bash + env: + HELM_CHART: ${{ env.RELEASE_HELM_PATH }} + RELEASE_TYPE: ${{ steps.plan.outputs.release_type }} + RELEASE_VERSION: ${{ steps.plan.outputs.version }} + NIGHTLY_TIMESTAMP: ${{ steps.plan.outputs.nightly_timestamp }} + HELM_VERSION_OVERRIDE: ${{ steps.plan.outputs.helm_version_override }} + run: | + set -euo pipefail + + if [[ -n "${HELM_VERSION_OVERRIDE}" ]]; then + helm_version="${HELM_VERSION_OVERRIDE}" + elif [[ "${RELEASE_TYPE}" == "nightly" ]]; then + # Nightlies align to the release core from the latest release or + # RC tag. Build-metadata chart fixes use helm-version instead. + base_version="$( + git tag --merged HEAD --list \ + | awk ' + /^[0-9]+\.[0-9]+\.[0-9]+(-rc[0-9]+)?$/ { + sub(/-rc[0-9]+$/, "") + print + } + ' \ + | sort -V -u \ + | tail -n 1 + )" + if [[ -z "${base_version}" ]]; then + base_version="$( + awk ' + $1 == "version:" { + version = $2 + gsub(/^["'\''"]|["'\''"]$/, "", version) + print version + exit + } + ' "${HELM_CHART}/Chart.yaml" + )" + fi + if [[ -z "${base_version}" ]]; then + echo "Unable to resolve Helm chart base version for nightly release." >&2 + exit 1 + fi + helm_version="${base_version}-nightly-${NIGHTLY_TIMESTAMP}" + else + helm_version="${RELEASE_VERSION}" + fi + + echo "helm_version=${helm_version}" >> "${GITHUB_OUTPUT}" + notify-start: # Start alerts intentionally run for dry runs so the webhook can be tested. name: Notify release start @@ -665,8 +726,6 @@ jobs: uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ needs.plan-release.outputs.source_sha }} - fetch-depth: 0 - fetch-tags: true persist-credentials: false - name: Set up Helm @@ -690,10 +749,8 @@ jobs: NIGHTLY_IMAGE_REGISTRY: ${{ env.RELEASE_NIGHTLY_CONTAINER_REGISTRY }} STABLE_IMAGE_REGISTRY: ${{ env.RELEASE_HELM_REGISTRY }} RELEASE_TYPE: ${{ needs.plan-release.outputs.release_type }} - RELEASE_VERSION: ${{ needs.plan-release.outputs.version }} RELEASE_LABEL: ${{ needs.plan-release.outputs.release_label }} - NIGHTLY_TIMESTAMP: ${{ needs.plan-release.outputs.nightly_timestamp }} - HELM_VERSION_OVERRIDE: ${{ needs.plan-release.outputs.helm_version_override }} + HELM_VERSION: ${{ needs.plan-release.outputs.helm_version }} run: | set -euo pipefail @@ -711,30 +768,7 @@ jobs: release_registry="${STABLE_IMAGE_REGISTRY}" fi - chart_version="${HELM_VERSION_OVERRIDE}" - if [[ -z "${chart_version}" ]]; then - if [[ "${RELEASE_TYPE}" == "nightly" ]]; then - # Nightlies align to the release core from the latest release or - # RC tag. Build-metadata chart fixes use helm-version instead. - base_version="$( - git tag --merged HEAD --list \ - | awk ' - /^[0-9]+\.[0-9]+\.[0-9]+(-rc[0-9]+)?$/ { - sub(/-rc[0-9]+$/, "") - print - } - ' \ - | sort -V -u \ - | tail -n 1 - )" - if [[ -z "${base_version}" ]]; then - base_version="$(yq -r '.version' "${chart_dir}/Chart.yaml")" - fi - chart_version="${base_version}-nightly-${NIGHTLY_TIMESTAMP}" - else - chart_version="${RELEASE_VERSION}" - fi - fi + chart_version="${HELM_VERSION}" yq -i ".platformConfig.platform.image_registry = \"${release_registry}\"" "${chart_dir}/values.yaml" yq -i ".api.image.repository = \"${release_registry}/nmp-api\"" "${chart_dir}/values.yaml" diff --git a/RELEASING.md b/RELEASING.md index 24d75f22862..420131495d1 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -76,21 +76,21 @@ America/Los_Angeles. ## What the workflow does -1. Resolves the source, release label, selected artifacts, and wheel version. - Stable versions use the supplied release version. Nightly labels use - `nightly-` and the wheel version is resolved by - `.github/scripts/stamp_sdk_version.py`. +1. Resolves the source, release label, selected artifacts, wheel version, and + Helm chart version. Stable versions use the supplied release version. + Nightly labels use `nightly-` and the wheel version is + resolved by `.github/scripts/stamp_sdk_version.py`. 2. Checks out the selected source and validates the selected wheel paths, Docker Bake targets, and NGC overview files. 3. Optionally synchronizes NGC metadata, when requested on a non-dry-run. 4. Dispatches wheel, container, and stable-release registration work to the configured internal release repository. The selected source SHA, release type, version, and selected IDs are passed with the dispatch. -5. Packages the Helm chart. A nightly chart uses the latest release or RC Git - tag core reachable from the selected source with `-nightly-` - appended, falling back to the `Chart.yaml` version if no such tag exists. A - stable chart uses the stable release version unless `helm-version` is set for - a stable Helm-only release. +5. Packages the Helm chart with the planned chart version. A nightly chart uses + the latest release or RC Git tag core reachable from the selected source with + `-nightly-` appended, falling back to the `Chart.yaml` + version if no such tag exists. A stable chart uses the stable release version + unless `helm-version` is set for a stable Helm-only release. 6. Waits for every selected final artifact to become public before continuing. The polling job times out after four hours and sends a Slack alert after two hours if it is still waiting.