Skip to content

Commit 051532c

Browse files
committed
feat: support post-release preparation
Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
1 parent e832880 commit 051532c

6 files changed

Lines changed: 195 additions & 41 deletions

File tree

.github/workflows/container-build.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,8 @@ jobs:
8585
type=raw,value=${{ matrix.variant }},enable=${{ startsWith(github.ref, 'refs/tags/v') }}
8686
type=raw,value=latest-${{ matrix.variant }},enable=${{ startsWith(github.ref, 'refs/tags/v') }}
8787
type=sha,prefix=sha-,suffix=-${{ matrix.variant }}
88-
type=semver,pattern={{version}}-${{ matrix.variant }}
89-
type=semver,pattern={{major}}.{{minor}}-${{ matrix.variant }}
88+
type=pep440,pattern={{version}}-${{ matrix.variant }}
89+
type=pep440,pattern={{major}}.{{minor}}-${{ matrix.variant }}
9090
labels: |
9191
org.opencontainers.image.version=${{ steps.package-version.outputs.version }}
9292
com.nvidia.nemo.safe-synthesizer.extra=${{ matrix.extra }}

.mise/tasks/release/prepare

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/usr/bin/env bash
22
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
33
# SPDX-License-Identifier: Apache-2.0
4-
#MISE description="Prepare the next rc0 release tag from local Git tags without creating, deleting, or pushing tags."
4+
#MISE description="Prepare the next release tag from local Git tags without creating, deleting, or pushing tags."
55

66
set -euo pipefail
77
source "${MISE_CONFIG_ROOT}/.mise/tasks/_lib.sh"

CONTRIBUTING.md

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -685,14 +685,14 @@ Before contributing, run `mise run format` and `mise run check`. See `AGENTS.md`
685685

686686
Pushing a `v*` tag starts two workflows. [`release.yml`](.github/workflows/release.yml)
687687
publishes the wheel to Test PyPI and PyPI, creates a GitHub release, and
688-
publishes versioned documentation for stable releases.
688+
publishes versioned documentation for final releases, including post-releases.
689689
[`container-build.yml`](.github/workflows/container-build.yml) publishes the
690690
CUDA image to GitHub Container Registry (GHCR).
691691

692692
Release versions follow [PEP 440](https://peps.python.org/pep-0440/) with major,
693-
minor, and patch release numbers. This project uses stable releases and release
694-
candidates only; prerelease versions append `rcN` without a dash. The GitHub tag
695-
always starts with a `v` prefix.
693+
minor, and patch release numbers. This project uses stable releases, release
694+
candidates, and post-releases. Prerelease versions append `rcN` without a dash;
695+
post-releases append `.postN`. The GitHub tag always starts with a `v` prefix.
696696

697697
Examples:
698698

@@ -702,9 +702,11 @@ Examples:
702702
| `v2.1.3` | `2.1.3` ||
703703
| `v0.0.5rc0` | `0.0.5rc0` ||
704704
| `v0.1.2rc5` | `0.1.2rc5` ||
705+
| `v0.1.6.post1` | `0.1.6.post1` ||
705706
| `1.0.0` | | ❌ No `v` prefix |
706707
| `release-1.0` | | ❌ Wrong format |
707708
| `v0.0.7-rc4` | | ❌ Dash before rc suffix |
709+
| `v0.1.6-post1` | | ❌ Dash before post-release suffix |
708710
| `v0.1.3a1` | | ❌ Alpha prereleases are not used; use rcN only |
709711

710712
### Release Checklist
@@ -713,8 +715,8 @@ Examples:
713715

714716
- Fetch `origin/main` and tags, choose the exact release commit, and confirm its
715717
normal CI and manually dispatched GPU Tests run passed.
716-
- Choose unused candidate and stable tags, then record the candidate's exact
717-
`origin/main` SHA.
718+
- Choose the unused tag or tags required for the release type, then record the
719+
exact `origin/main` SHA.
718720
- Decide whether GHCR visibility or a separate nSpect or Pulse scan blocks the
719721
release. Those scans are not part of the GitHub release workflows.
720722

@@ -785,21 +787,48 @@ git tag "${STABLE_TAG}" "${RELEASE_SHA}"
785787
git push origin "refs/tags/${STABLE_TAG}"
786788
```
787789

788-
#### After Publishing Stable
790+
#### Post-release
791+
792+
Use a post-release for a packaging or release correction that does not warrant
793+
a new regular patch version. A post-release is final, so it does not use the
794+
release-candidate promotion sequence. Preview the next post-release tag and
795+
resolve its target commit without creating or pushing a tag:
796+
797+
```bash
798+
mise run release:prepare -- --bump post --ref origin/main
799+
```
800+
801+
After reviewing the output, create the proposed tag at the resolved commit and
802+
push it. For example:
803+
804+
```bash
805+
POST_TAG=v0.1.6.post1
806+
RELEASE_SHA="$(git rev-parse 'origin/main^{commit}')"
807+
git tag "${POST_TAG}" "${RELEASE_SHA}"
808+
git push origin "refs/tags/${POST_TAG}"
809+
```
810+
811+
The container workflow publishes `X.Y.Z.postN-cu129`, the immutable
812+
`sha-<short-sha>-cu129` tag, and the mutable `cu129` and `latest-cu129` aliases.
813+
PEP 440 post-releases do not move the shortened `X.Y-cu129` tag. Validate the
814+
post-release wheel and immutable container tag before announcing the release.
815+
816+
#### After Publishing a Final Release
789817

790818
After publishing:
791819

792820
- Verify both tag-triggered workflows passed at the tested SHA.
793-
- Confirm Test PyPI and production PyPI contain the stable version.
821+
- Confirm Test PyPI and production PyPI contain the published version.
794822
- Confirm the GitHub release is not marked as a prerelease.
795823
- Confirm versioned documentation is available at
796824
`https://nvidia-nemo.github.io/Safe-Synthesizer/<version>/`.
797-
- Confirm GHCR exposes the stable `X.Y.Z-cu129` and `X.Y-cu129` tags with the
798-
intended visibility.
825+
- Confirm GHCR exposes the expected tags with the intended visibility. Regular
826+
stable releases publish `X.Y.Z-cu129` and `X.Y-cu129`; post-releases publish
827+
`X.Y.Z.postN-cu129` without moving `X.Y-cu129`.
799828
- Coordinate a NeMo Platform package or container pin, documentation update,
800829
and downstream release when Platform should consume the new version. This is
801830
not currently automated by the Safe Synthesizer release workflow.
802-
- Announce the release only after artifacts and stable documentation pass
831+
- Announce the release only after artifacts and versioned documentation pass
803832
verification.
804833

805834
## NMP Integration

tests/tools/test_release_version.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,38 @@ def test_major_minor_and_patch_bumps_prepare_rc0(self) -> None:
8585
plan = release_version.plan_release(bump=bump, cwd=self.repo)
8686
self.assertEqual((plan.next_tag, plan.next_version), expected)
8787

88+
def test_post_bump_prepares_first_post_release(self) -> None:
89+
self._tag("v1.5.0")
90+
self._tag("v1.6.0")
91+
92+
plan = release_version.plan_release(bump="post", cwd=self.repo)
93+
94+
self.assertEqual(plan.latest_stable_tag, "v1.6.0")
95+
self.assertEqual(plan.latest_stable_version, "1.6.0")
96+
self.assertEqual(plan.bump, "post")
97+
self.assertEqual(plan.next_tag, "v1.6.0.post1")
98+
self.assertEqual(plan.next_version, "1.6.0.post1")
99+
100+
def test_post_bump_increments_highest_post_for_latest_stable(self) -> None:
101+
self._tag("v1.5.0")
102+
self._tag("v1.5.0.post7")
103+
self._tag("v1.6.0")
104+
self._tag("v1.6.0.post1")
105+
self._tag("v1.6.0.post10")
106+
107+
plan = release_version.plan_release(bump="post", cwd=self.repo)
108+
109+
self.assertEqual(plan.next_tag, "v1.6.0.post11")
110+
111+
def test_regular_patch_accepts_post_tags_and_uses_stable_base(self) -> None:
112+
self._tag("v1.6.0")
113+
self._tag("v1.6.0.post1")
114+
115+
plan = release_version.plan_release(cwd=self.repo)
116+
117+
self.assertEqual(plan.latest_stable_tag, "v1.6.0")
118+
self.assertEqual(plan.next_tag, "v1.6.1rc0")
119+
88120
def test_json_cli_resolves_candidate_ref(self) -> None:
89121
self._tag("v0.1.0")
90122
first_commit = self._git("rev-parse", "HEAD")
@@ -110,6 +142,25 @@ def test_human_cli_does_not_create_candidate_tag(self) -> None:
110142
self.assertIn("No tag was created, deleted, or pushed.", result.stdout)
111143
self.assertEqual(self._git("tag", "--list"), tags_before)
112144

145+
def test_human_cli_describes_post_release_without_creating_tag(self) -> None:
146+
self._tag("v1.6.0")
147+
tags_before = self._git("tag", "--list")
148+
149+
result = self._run(sys.executable, str(TOOL_PATH), "--bump", "post")
150+
151+
self.assertEqual(result.returncode, 0, result.stderr)
152+
self.assertIn("Next Safe-Synthesizer post-release", result.stdout)
153+
self.assertIn("Next post-release tag: v1.6.0.post1", result.stdout)
154+
self.assertIn("No tag was created, deleted, or pushed.", result.stdout)
155+
self.assertEqual(self._git("tag", "--list"), tags_before)
156+
157+
def test_post_release_without_matching_stable_tag_is_refused(self) -> None:
158+
self._tag("v1.5.0")
159+
self._tag("v1.6.0.post1")
160+
161+
with self.assertRaisesRegex(release_version.ReleaseVersionError, "missing stable tag v1.6.0"):
162+
release_version.plan_release(bump="post", cwd=self.repo)
163+
113164
def test_malformed_v_tag_is_an_explicit_error(self) -> None:
114165
self._tag("v0.1.0")
115166
self._tag("v0.2.0-rc0")
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
from pathlib import Path
5+
6+
import yaml
7+
8+
WORKFLOW = Path(__file__).parents[2] / ".github" / "workflows" / "container-build.yml"
9+
10+
11+
def test_container_tags_use_pep440_versions() -> None:
12+
workflow = yaml.load(WORKFLOW.read_text(), Loader=yaml.BaseLoader)
13+
steps = workflow["jobs"]["build"]["steps"]
14+
metadata_step = next(step for step in steps if step.get("name") == "Extract image metadata")
15+
tags = metadata_step["with"]["tags"].splitlines()
16+
17+
assert "type=pep440,pattern={{version}}-${{ matrix.variant }}" in tags
18+
assert "type=pep440,pattern={{major}}.{{minor}}-${{ matrix.variant }}" in tags
19+
assert not any(tag.startswith("type=semver") for tag in tags)

0 commit comments

Comments
 (0)