diff --git a/README.md b/README.md
index a1be8be..bf894e7 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
-
+
RAMPART
diff --git a/docs/contributing/release-process.md b/docs/contributing/release-process.md
index 08c6609..cf12883 100644
--- a/docs/contributing/release-process.md
+++ b/docs/contributing/release-process.md
@@ -34,23 +34,29 @@ If you find functionality to remove, merge the removal PR to `main` before proce
## 4. Update the Version
-### pyproject.toml
-Set the version in `pyproject.toml` to the version established in step 2.
+### Git tag
+RAMPART derives package versions from Git tags using Hatch VCS and setuptools-scm. No `pyproject.toml` version bump is required for a release. The release version is determined by the `vx.y.z` tag pushed in step 5.
```toml
-[project]
-name = "RAMPART"
-version = "x.y.z"
+[tool.hatch.version]
+source = "vcs"
+
+[tool.hatch.version.raw-options]
+local_scheme = "no-local-version"
```
+The `no-local-version` setting omits local version suffixes such as `+g` because PyPI does not support them for upstream releases. See the [setuptools-scm local scheme documentation](https://setuptools-scm.readthedocs.io/en/latest/extending/#setuptools_scmlocal_scheme) for details.
+
+For development builds on `main`, the release tag must be reachable from `main` history for Hatch VCS to infer the next development version from that tag. If the release branch contains commits beyond `main`, merge or cherry-pick those release commits back to `main` after publishing.
+
### Update README File
The README file is published to PyPI and also needs to be updated so the links work properly. _Note: There may not be any links to update, but it is good practice to check in case our README changes._
-Replace all “main” links like “doc/index.md” with “raw” links that have the correct version number, i.e., “https://raw.githubusercontent.com/microsoft/RAMPART/releases/vx.y.z/docs/index.md”.
+Keep README image links relative when they point to files in this repository, e.g., `docs/images/RAMPART.svg`. During package builds, `scripts/hatch_build.py` generates the PyPI README metadata and rewrites those image paths to raw GitHub URLs with the release version.
-For images, update using the “raw” link, e.g., “https://raw.githubusercontent.com/microsoft/RAMPART/releases/vx.y.z/docs/images/RAMPART.png”.
+Replace any other "main" links like "doc/index.md" with "raw" links that have the correct version number, i.e., "https://raw.githubusercontent.com/microsoft/RAMPART/releases/vx.y.z/docs/index.md".
-For directories, update using the “tree” link, e.g., “https://github.com/microsoft/RAMPART/tree/releases/vx.y.z/docs/usage"
+For directories, update using the "tree" link, e.g., "https://github.com/microsoft/RAMPART/tree/releases/vx.y.z/docs/usage"
This is required for the release branch because PyPI does not pick up other files besides the README, which results in local links breaking.
@@ -149,7 +155,7 @@ If successful, the URL `https://pypi.org/project/rampart/x.y.z/` will return the
After the release is on PyPI, open a PR to `main` containing only:
-- In line with PyPA [versioning guidance](https://packaging.python.org/en/latest/discussions/versioning/), bump the version in `pyproject.toml` to the next development version (e.g., `x.y.(z+1).dev0` or `x.(y+1).0.dev0`, depending on the next planned release).
+- Any follow-up documentation or metadata updates needed after the release. Do not bump the package version in `pyproject.toml`; once `main` has commits after the release tag, Hatch VCS will infer the next development version automatically.
- Replace any references to the previous release version in the codebase with the new released version (without `.dev0`) where applicable (e.g., installation docs that pin to the latest tag).
Open this PR from a branch separate from your `releases/vx.y.z` branch.
@@ -216,10 +222,10 @@ A patch release (e.g., `0.2.0` → `0.2.1`) ships a targeted fix — typically a
Resolve any conflicts manually. Patch-sized fixes typically apply cleanly.
-3. **Bump the version** in `pyproject.toml` to the new patch version. Also update any version-pinned links in `README.md`.
+3. **Update release-specific references** as needed. Do not bump the package version in `pyproject.toml`; the patch version comes from the `vx.y.z` tag. Also update any version-pinned links in `README.md`.
```bash
- git commit -am "Bump version to x.y.z"
+ git commit -am "Prepare x.y.z release"
```
4. **Push and tag**:
diff --git a/pyproject.toml b/pyproject.toml
index bdf8ee6..811549c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,12 +1,11 @@
[build-system]
-requires = ["setuptools"]
-build-backend = "setuptools.build_meta"
+requires = ["hatchling>=1.30.1", "hatch-vcs>=0.5.0"]
+build-backend = "hatchling.build"
[project]
name = "RAMPART"
-version = "0.1.1.dev0"
description = "A pytest-native safety testing framework for agentic AI applications"
-readme = "README.md"
+dynamic = ["readme", "version"]
license = "MIT"
requires-python = ">=3.11"
authors = [
@@ -42,6 +41,8 @@ onedrive = [
[dependency-groups]
dev = [
+ "hatch-vcs>=0.5.0",
+ "hatchling>=1.30.1",
"pre-commit>=4.5.1",
"pytest-cov>=6.1.0",
"pytest-xdist[psutil]>=3.8.0",
@@ -63,14 +64,17 @@ Issues = "https://github.com/microsoft/RAMPART/issues"
[project.entry-points.pytest11]
rampart = "rampart.pytest_plugin.plugin"
-[tool.setuptools.packages.find]
-exclude = ["site*", "docs*", "tests*", "scripts*"]
+[tool.hatch.metadata.hooks.custom]
+path = "scripts/hatch_build.py"
-[tool.setuptools.package-data]
-rampart = [
- "drivers/prompts/*.yaml",
- "evaluators/prompts/*.yaml",
-]
+[tool.hatch.version]
+source = "vcs"
+
+[tool.hatch.version.raw-options]
+local_scheme = "no-local-version"
+
+[tool.hatch.build.targets.wheel]
+packages = ["rampart"]
[tool.coverage.run]
source = ["rampart"]
@@ -97,6 +101,9 @@ filterwarnings = [
select = ["ALL"]
[tool.ruff.lint.per-file-ignores]
+"scripts/hatch_build.py" = [
+ "INP001", # Top-level build hook
+]
"tests/**" = [
"S101", # assert is pytest's API
"D100", "D101", "D102", "D104", "D107", # no docstrings needed
@@ -130,7 +137,7 @@ max-args = 10
python-version = "3.11"
[tool.ty.src]
-include = ["rampart", "tests"]
+include = ["rampart", "tests", "scripts"]
[tool.uv.sources]
pyrit = { git = "https://github.com/microsoft/PyRIT", rev = "6dc8b94139757390286bbce7d53c1f7e58e66e29" } # v0.13.0
diff --git a/scripts/hatch_build.py b/scripts/hatch_build.py
new file mode 100644
index 0000000..70e6a4c
--- /dev/null
+++ b/scripts/hatch_build.py
@@ -0,0 +1,81 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT license.
+"""Hatchling metadata hooks for RAMPART package builds."""
+
+from __future__ import annotations
+
+import re
+from pathlib import Path
+
+from hatchling.metadata.plugin.interface import MetadataHookInterface
+
+_GITHUB_IMAGE_URL_PATTERNS = (
+ re.compile(r"(https://github\.com/microsoft/RAMPART/raw/)main(/docs/images/)"),
+ re.compile(
+ r"(https://raw\.githubusercontent\.com/microsoft/RAMPART/)main(/docs/images/)",
+ ),
+)
+_RELATIVE_HTML_IMAGE_URL_PATTERNS = (
+ re.compile(r'(
]*\bsrc=")(?:\./)?(docs/images/[^"]+)(")'),
+ re.compile(r"(
]*\bsrc=')(?:\./)?(docs/images/[^']+)(')"),
+)
+_RELATIVE_MARKDOWN_IMAGE_URL_PATTERN = re.compile(
+ r"(!\[[^\]]*\]\()(?:\./)?(docs/images/[^)]+)(\))",
+)
+
+
+def _readme_ref(version: str) -> str:
+ """Return the Git ref to use for README image URLs."""
+ if ".dev" in version or "+" in version:
+ return "main"
+
+ return f"v{version}"
+
+
+def _raw_image_url(*, readme_ref: str, image_path: str) -> str:
+ """Return an absolute GitHub raw URL for a README image."""
+ return (
+ f"https://raw.githubusercontent.com/microsoft/RAMPART/{readme_ref}/{image_path}"
+ )
+
+
+def _render_readme(*, root: Path, version: str) -> str:
+ """Render README content for package metadata."""
+ readme = (root / "README.md").read_text(encoding="utf-8")
+ readme_ref = _readme_ref(version)
+
+ for pattern in _GITHUB_IMAGE_URL_PATTERNS:
+ readme = pattern.sub(rf"\g<1>{readme_ref}\g<2>", readme)
+
+ for pattern in _RELATIVE_HTML_IMAGE_URL_PATTERNS:
+ readme = pattern.sub(
+ lambda match: (
+ f"{match.group(1)}"
+ f"{_raw_image_url(readme_ref=readme_ref, image_path=match.group(2))}"
+ f"{match.group(3)}"
+ ),
+ readme,
+ )
+
+ return _RELATIVE_MARKDOWN_IMAGE_URL_PATTERN.sub(
+ lambda match: (
+ f"{match.group(1)}"
+ f"{_raw_image_url(readme_ref=readme_ref, image_path=match.group(2))}"
+ f"{match.group(3)}"
+ ),
+ readme,
+ )
+
+
+class ReadmeMetadataHook(MetadataHookInterface):
+ """Generate PyPI README metadata with release-pinned image URLs."""
+
+ def update(self, metadata: dict[str, object]) -> None:
+ """Update project metadata in-place."""
+ metadata["readme"] = {
+ "content-type": "text/markdown",
+ "text": _render_readme(
+ root=Path(self.root),
+ version=str(metadata["version"]),
+ ),
+ }
diff --git a/uv.lock b/uv.lock
index 2f4bd07..70cefe6 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1061,6 +1061,34 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" },
]
+[[package]]
+name = "hatch-vcs"
+version = "0.5.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "hatchling" },
+ { name = "setuptools-scm" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/6b/b0/4cc743d38adbee9d57d786fa496ed1daadb17e48589b6da8fa55717a0746/hatch_vcs-0.5.0.tar.gz", hash = "sha256:0395fa126940340215090c344a2bf4e2a77bcbe7daab16f41b37b98c95809ff9", size = 11424, upload-time = "2025-05-27T05:16:04.49Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5f/48/1f85cee4b7b4f40b9b814b1febbc661bda6ced9649e410a0b74f6e415dd0/hatch_vcs-0.5.0-py3-none-any.whl", hash = "sha256:b49677dbdc597460cc22d01b27ab3696f5e16a21ecf2700fb01bc28e1f2a04a7", size = 8507, upload-time = "2025-05-27T05:16:03.184Z" },
+]
+
+[[package]]
+name = "hatchling"
+version = "1.30.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "packaging" },
+ { name = "pathspec" },
+ { name = "pluggy" },
+ { name = "trove-classifiers" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/63/4c/8717ccb844b4fa5a5ba6352e97d743ed24e9a22cf90b7c109c17030a46a1/hatchling-1.30.1.tar.gz", hash = "sha256:eee4fd45357f72ebb3d7a42e5d72cfb5e29ed426d79e8836288926c4258d5f2e", size = 56929, upload-time = "2026-06-02T00:09:41.487Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/56/49/2797ec0ef88008a653a8867bb8d1e5c223cd2df8e40390dd5c6a0279cbc5/hatchling-1.30.1-py3-none-any.whl", hash = "sha256:161eacafb3c6f91526e92116d21426369f2c36e98c36a864f11a96345ad4ee31", size = 77489, upload-time = "2026-06-02T00:09:40.139Z" },
+]
+
[[package]]
name = "hf-xet"
version = "1.5.1"
@@ -2985,7 +3013,6 @@ wheels = [
[[package]]
name = "rampart"
-version = "0.1.1.dev0"
source = { editable = "." }
dependencies = [
{ name = "jinja2" },
@@ -3003,6 +3030,8 @@ onedrive = [
[package.dev-dependencies]
dev = [
+ { name = "hatch-vcs" },
+ { name = "hatchling" },
{ name = "pre-commit" },
{ name = "pytest-cov" },
{ name = "pytest-xdist", extra = ["psutil"] },
@@ -3030,6 +3059,8 @@ provides-extras = ["onedrive"]
[package.metadata.requires-dev]
dev = [
+ { name = "hatch-vcs", specifier = ">=0.5.0" },
+ { name = "hatchling", specifier = ">=1.30.1" },
{ name = "pre-commit", specifier = ">=4.5.1" },
{ name = "pytest-cov", specifier = ">=6.1.0" },
{ name = "pytest-xdist", extras = ["psutil"], specifier = ">=3.8.0" },
@@ -3317,6 +3348,29 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d6/02/12c73fd423eb9577b97fc1924966b929eff7074ae6b2e15dd3d30cb9e4ae/segno-1.6.6-py3-none-any.whl", hash = "sha256:28c7d081ed0cf935e0411293a465efd4d500704072cdb039778a2ab8736190c7", size = 76503, upload-time = "2025-03-12T22:12:48.106Z" },
]
+[[package]]
+name = "setuptools"
+version = "82.0.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" },
+]
+
+[[package]]
+name = "setuptools-scm"
+version = "10.1.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "packaging" },
+ { name = "setuptools" },
+ { name = "vcs-versioning" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a6/3e/edb74671eca6429f375244d1d6395c11b7d4832cda772e4c630141e121c7/setuptools_scm-10.1.1.tar.gz", hash = "sha256:c9eed4754da1a25016d49c1b3cd09c7c8e65f816b5afb8195bf2ac3c6748f23a", size = 66514, upload-time = "2026-06-22T14:15:44.086Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fa/a8/3e86057d0d6274e57b9b0b40cb14b90832552c33a8f855b3675843686284/setuptools_scm-10.1.1-py3-none-any.whl", hash = "sha256:4660e6a3b1764ff4b11188de93c26f02396ef294784a75e031e0a5f60c2379fe", size = 27692, upload-time = "2026-06-22T14:15:42.804Z" },
+]
+
[[package]]
name = "shellingham"
version = "1.5.4"
@@ -3574,6 +3628,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2e/24/32361f5d0e2eff7ff1881ac6833b6b090cfe34515b1ee9082636cbe69442/treelib-1.8.0-py3-none-any.whl", hash = "sha256:5235d1ebf988c5026f26ce6e5e0cd470007f16d4978185f5c9b3eee8a25aef81", size = 30728, upload-time = "2025-06-29T15:06:48.248Z" },
]
+[[package]]
+name = "trove-classifiers"
+version = "2026.6.1.19"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c2/e3/7ca82ee24c82d344584abd5b8637b3bd056f2900226e8d82fc22f1184b92/trove_classifiers-2026.6.1.19.tar.gz", hash = "sha256:c5132b4b61a829d11cfbd2d72e97f20a45ed6edb95e45c5efdeb5e00836b2745", size = 17059, upload-time = "2026-06-01T19:41:34.649Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7c/a4/81502f486f01db95bc8320646a8a12511f5e556cb63d5e224d91816605c4/trove_classifiers-2026.6.1.19-py3-none-any.whl", hash = "sha256:ab4c4ec93cc4a4e7815fa759906e05e6bb3f2fbd92ea0f897288c6a43efd15b3", size = 14211, upload-time = "2026-06-01T19:41:33.434Z" },
+]
+
[[package]]
name = "ty"
version = "0.0.50"
@@ -3715,6 +3778,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" },
]
+[[package]]
+name = "vcs-versioning"
+version = "2.0.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "packaging" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c6/5d/e6c5d8be9f637b7ac6cb83c2c51675c431092b76543866f9c152f3321a76/vcs_versioning-2.0.1.tar.gz", hash = "sha256:0e827e50ff98c3b74961bc9bab1bdb494d6ef9f9624bc0466afbafef23ff0d8c", size = 127905, upload-time = "2026-06-22T14:15:51.531Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/36/51/b9b812b8d09584d8cdfc4c19328e5a86e72f6da310d529cc009d194ac6a7/vcs_versioning-2.0.1-py3-none-any.whl", hash = "sha256:fa1a7e49745fb968af54d1422e1f6bcde2563046f2c283a1a8d720184ece6ea1", size = 105175, upload-time = "2026-06-22T14:15:50.132Z" },
+]
+
[[package]]
name = "virtualenv"
version = "21.5.1"