From 5e6430b88ff6b84788a85b369d98f39d451c3f02 Mon Sep 17 00:00:00 2001 From: spencrr <23708360+spencrr@users.noreply.github.com> Date: Mon, 22 Jun 2026 14:01:37 -0700 Subject: [PATCH 1/3] [META]: Generate README dynamically for functional image permalinks on PyPI release --- README.md | 2 +- docs/contributing/release-process.md | 4 +- pyproject.toml | 23 ++++---- scripts/hatch_build.py | 81 ++++++++++++++++++++++++++++ uv.lock | 26 +++++++++ 5 files changed, 122 insertions(+), 14 deletions(-) create mode 100644 scripts/hatch_build.py diff --git a/README.md b/README.md index a1be8be..bf894e7 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- RAMPART Logo + RAMPART Logo

RAMPART

diff --git a/docs/contributing/release-process.md b/docs/contributing/release-process.md index 08c6609..c0e5274 100644 --- a/docs/contributing/release-process.md +++ b/docs/contributing/release-process.md @@ -46,9 +46,9 @@ version = "x.y.z" ### 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" diff --git a/pyproject.toml b/pyproject.toml index bdf8ee6..43f6a40 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,12 @@ [build-system] -requires = ["setuptools"] -build-backend = "setuptools.build_meta" +requires = ["hatchling>=1.30.1"] +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"] license = "MIT" requires-python = ">=3.11" authors = [ @@ -42,6 +42,7 @@ onedrive = [ [dependency-groups] dev = [ + "hatchling>=1.30.1", "pre-commit>=4.5.1", "pytest-cov>=6.1.0", "pytest-xdist[psutil]>=3.8.0", @@ -63,14 +64,11 @@ 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.build.targets.wheel] +packages = ["rampart"] [tool.coverage.run] source = ["rampart"] @@ -97,6 +95,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 +131,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..584d12c 100644 --- a/uv.lock +++ b/uv.lock @@ -1061,6 +1061,21 @@ 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 = "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" @@ -3003,6 +3018,7 @@ onedrive = [ [package.dev-dependencies] dev = [ + { name = "hatchling" }, { name = "pre-commit" }, { name = "pytest-cov" }, { name = "pytest-xdist", extra = ["psutil"] }, @@ -3030,6 +3046,7 @@ provides-extras = ["onedrive"] [package.metadata.requires-dev] dev = [ + { 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" }, @@ -3574,6 +3591,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" From 55b562d7b71f74fad56f692250725fbf4a3eab1a Mon Sep 17 00:00:00 2001 From: spencrr <23708360+spencrr@users.noreply.github.com> Date: Mon, 22 Jun 2026 14:13:05 -0700 Subject: [PATCH 2/3] [META]: Derive package version from Git tags --- docs/contributing/release-process.md | 22 +++++++----- pyproject.toml | 12 +++++-- uv.lock | 51 +++++++++++++++++++++++++++- 3 files changed, 73 insertions(+), 12 deletions(-) diff --git a/docs/contributing/release-process.md b/docs/contributing/release-process.md index c0e5274..9d00b86 100644 --- a/docs/contributing/release-process.md +++ b/docs/contributing/release-process.md @@ -34,15 +34,21 @@ 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._ @@ -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 43f6a40..811549c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,11 @@ [build-system] -requires = ["hatchling>=1.30.1"] +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" -dynamic = ["readme"] +dynamic = ["readme", "version"] license = "MIT" requires-python = ">=3.11" authors = [ @@ -42,6 +41,7 @@ onedrive = [ [dependency-groups] dev = [ + "hatch-vcs>=0.5.0", "hatchling>=1.30.1", "pre-commit>=4.5.1", "pytest-cov>=6.1.0", @@ -67,6 +67,12 @@ rampart = "rampart.pytest_plugin.plugin" [tool.hatch.metadata.hooks.custom] path = "scripts/hatch_build.py" +[tool.hatch.version] +source = "vcs" + +[tool.hatch.version.raw-options] +local_scheme = "no-local-version" + [tool.hatch.build.targets.wheel] packages = ["rampart"] diff --git a/uv.lock b/uv.lock index 584d12c..70cefe6 100644 --- a/uv.lock +++ b/uv.lock @@ -1061,6 +1061,19 @@ 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" @@ -3000,7 +3013,6 @@ wheels = [ [[package]] name = "rampart" -version = "0.1.1.dev0" source = { editable = "." } dependencies = [ { name = "jinja2" }, @@ -3018,6 +3030,7 @@ onedrive = [ [package.dev-dependencies] dev = [ + { name = "hatch-vcs" }, { name = "hatchling" }, { name = "pre-commit" }, { name = "pytest-cov" }, @@ -3046,6 +3059,7 @@ 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" }, @@ -3334,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" @@ -3741,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" From b779d5d9806a2fa5a12a64e089e87206c00281ce Mon Sep 17 00:00:00 2001 From: spencrr <23708360+spencrr@users.noreply.github.com> Date: Mon, 22 Jun 2026 14:21:36 -0700 Subject: [PATCH 3/3] fixup quotes --- docs/contributing/release-process.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/contributing/release-process.md b/docs/contributing/release-process.md index 9d00b86..cf12883 100644 --- a/docs/contributing/release-process.md +++ b/docs/contributing/release-process.md @@ -54,9 +54,9 @@ The README file is published to PyPI and also needs to be updated so the links w 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. -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”. +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.