From 6176993157bf8d4f132431e20b9b27e793e480fb Mon Sep 17 00:00:00 2001 From: Tobias Macey Date: Wed, 30 Sep 2026 10:20:56 -0400 Subject: [PATCH 1/5] feat: collect scriv changelog fragments when a release is cut Releases are cut by the ol-concourse release job, which runs `bump-my-version bump --no-commit` and then commits with `git add -u`. This adds a bump-my-version pre-commit hook that installs uv and runs bin/collect-changelog, which folds changelog.d/ fragments into CHANGELOG.md under the new version. A release with no fragments is a no-op rather than an error, because `scriv collect` exits 2 in that case and would otherwise fail Renovate-only releases. Claude-Session: https://claude.ai/code/session_01VaJ4VtbQFPV63VWtQV6JEg --- README.md | 19 +++ bin/collect-changelog | 24 ++++ changelog.d/.gitkeep | 0 ...0930_101301_blarghmatey_scriv_changelog.md | 3 + pyproject.toml | 25 ++++ uv.lock | 110 ++++++++++++++++++ 6 files changed, 181 insertions(+) create mode 100755 bin/collect-changelog create mode 100644 changelog.d/.gitkeep create mode 100644 changelog.d/20260930_101301_blarghmatey_scriv_changelog.md diff --git a/README.md b/README.md index b3f8316..c836c49 100644 --- a/README.md +++ b/README.md @@ -218,6 +218,25 @@ uv run ruff check . uv run mypy src ``` +## Changelog + +A change that someone deploying or consuming the API would notice should carry +a changelog fragment: + +```bash +uv run scriv create +``` + +This writes a file under `changelog.d/` with a commented-out section per +category (Added, Changed, Deprecated, Removed, Fixed, Security). Uncomment the +one that fits, write the entry, and commit it with the change. + +Nothing else needs doing at release time. The Concourse release job runs +bump-my-version, whose `pre_commit_hooks` run `bin/collect-changelog`, which +folds every fragment into `CHANGELOG.md` under the new version and deletes the +fragments in the same release commit. A release with no fragments still goes +out, with no changelog entry. + ## The published API contract Each tenant's OpenAPI document is committed under `openapi/specs/.yaml` diff --git a/bin/collect-changelog b/bin/collect-changelog new file mode 100755 index 0000000..bc8d392 --- /dev/null +++ b/bin/collect-changelog @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +"""Collect changelog.d/ fragments into CHANGELOG.md, if there are any. + +Run by bump-my-version's pre_commit_hooks when the release job bumps the +version (see [tool.bumpversion] in pyproject.toml). + +`scriv collect` exits 2 when there is nothing to collect, and a failing hook +fails the release. A release made only of changes that carry no fragment (e.g. +a batch of Renovate updates) is still a release, so an empty changelog.d/ is a +no-op here rather than an error. +""" + +from __future__ import annotations + +import sys + +from scriv.collect import collect +from scriv.scriv import Scriv + +if __name__ == "__main__": + if not Scriv().fragments_to_combine(): + sys.stdout.write("No changelog fragments to collect\n") + raise SystemExit(0) + collect.main(args=[]) diff --git a/changelog.d/.gitkeep b/changelog.d/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/changelog.d/20260930_101301_blarghmatey_scriv_changelog.md b/changelog.d/20260930_101301_blarghmatey_scriv_changelog.md new file mode 100644 index 0000000..cbd3aa8 --- /dev/null +++ b/changelog.d/20260930_101301_blarghmatey_scriv_changelog.md @@ -0,0 +1,3 @@ +### Added + +- Changelog fragments with scriv. Each PR adds a fragment under `changelog.d/`, and the release job collects them into `CHANGELOG.md` during the version bump. diff --git a/pyproject.toml b/pyproject.toml index 0600442..b9e8b35 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,13 @@ mitol-django-scim = "0d" mitol-drf-lint = "0d" [dependency-groups] +# scriv sits in its own group so the release task can install it without the +# rest of the dev toolchain (see pre_commit_hooks under [tool.bumpversion]). +release = [ + "scriv>=1.8.0", +] dev = [ + { include-group = "release" }, "ruff>=0.8", "mypy>=1.13", "pytest>=8.3", @@ -141,12 +147,31 @@ plugins = ["pydantic.mypy"] module = ["aiomysql", "aiomysql.*"] ignore_missing_imports = true +[tool.scriv] +format = "md" +fragment_directory = "changelog.d" +output_file = "CHANGELOG.md" +version = "literal: pyproject.toml: project.version" +categories = ["Added", "Changed", "Deprecated", "Removed", "Fixed", "Security"] +md_header_level = "2" +entry_title_template = "{{ version }} ({{ date.strftime('%Y-%m-%d') }})" + [tool.bumpversion] current_version = "2026.9.30.1" commit = false tag = false parse = "(?P(?:[1-9][0-9]{3})\\.(?:1[0-2]|[1-9])\\.(?:3[0-1]|[12][0-9]|[1-9]))\\.(?P\\d+)" serialize = ["{release}.{build}"] +# The ol-concourse release job runs `bump-my-version bump --no-commit` in an +# image that has pip but neither uv nor scriv, then the release resource commits +# the tree with `git add -u`. Pre-commit hooks still run under --no-commit, after +# pyproject.toml carries the new version, so scriv reads it from there. +# `git add -u` stages only tracked files, so CHANGELOG.md has to exist on main +# before a release rather than be created by one. +pre_commit_hooks = [ + "python3 -m pip install --quiet --root-user-action=ignore uv", + "uv run --frozen --only-group release bin/collect-changelog", +] [tool.bumpversion.parts.release] calver_format = "{YYYY}.{MM}.{DD}" diff --git a/uv.lock b/uv.lock index 861692a..6f5c56a 100644 --- a/uv.lock +++ b/uv.lock @@ -352,6 +352,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" }, ] +[[package]] +name = "click-log" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/32/32/228be4f971e4bd556c33d52a22682bfe318ffe57a1ddb7a546f347a90260/click-log-0.4.0.tar.gz", hash = "sha256:3970f8570ac54491237bcdb3d8ab5e3eef6c057df29f8c3d1151a51a9c23b975", size = 9985, upload-time = "2022-03-13T11:10:15.262Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/5a/4f025bc751087833686892e17e7564828e409c43b632878afeae554870cd/click_log-0.4.0-py2.py3-none-any.whl", hash = "sha256:a43e394b528d52112af599f2fc9e4b7cf3c15f94e53581f74fa6867e68c91756", size = 4273, upload-time = "2022-03-13T11:10:17.594Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -713,6 +725,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + [[package]] name = "librt" version = "0.15.0" @@ -826,6 +850,69 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, ] +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + [[package]] name = "mdurl" version = "0.1.2" @@ -931,10 +1018,14 @@ dev = [ { name = "pytest-httpx" }, { name = "pyyaml" }, { name = "ruff" }, + { name = "scriv" }, { name = "types-cachetools" }, { name = "types-hvac" }, { name = "types-pyyaml" }, ] +release = [ + { name = "scriv" }, +] [package.metadata] requires-dist = [ @@ -966,10 +1057,12 @@ dev = [ { name = "pytest-httpx", specifier = ">=0.35" }, { name = "pyyaml", specifier = ">=6.0.3" }, { name = "ruff", specifier = ">=0.8" }, + { name = "scriv", specifier = ">=1.8.0" }, { name = "types-cachetools", specifier = ">=5.5" }, { name = "types-hvac", specifier = ">=2.3" }, { name = "types-pyyaml", specifier = ">=6.0.12.20260724" }, ] +release = [{ name = "scriv", specifier = ">=1.8.0" }] [[package]] name = "opentelemetry-api" @@ -1465,6 +1558,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fe/a0/50787329e4f20bf9dc9f6230015d46ec69c51a97ace5bc202dae4755365d/ruff-0.16.8-py3-none-win_arm64.whl", hash = "sha256:d075e820af612102ce217f07cc93e69f9490b10ec13ea85fa87bd03d996cef8a", size = 10386316, upload-time = "2026-09-16T15:54:43.332Z" }, ] +[[package]] +name = "scriv" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "click" }, + { name = "click-log" }, + { name = "jinja2" }, + { name = "markdown-it-py" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/14/9a/2ef2209e0672b264a2f2574dc88ea3cd9cfc9adfecbfd3165a900980ec8c/scriv-1.8.0.tar.gz", hash = "sha256:7b1a105dd411ac541998250fc8594742419f94cee984ca1257c5ebf5af21918b", size = 98160, upload-time = "2025-12-30T00:01:10.13Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/e7/062480ede84ecb56ee0f8f2e5b5a3b2a5bceeb73bbdf909d3c13f5438749/scriv-1.8.0-py3-none-any.whl", hash = "sha256:f00f51325b2f4bc96b16fbb1239d4ab577cc2422301a5dd4f5f9378aae2549e0", size = 39085, upload-time = "2025-12-30T00:01:08.599Z" }, +] + [[package]] name = "sentry-sdk" version = "2.69.2" From 73c567d9dfaaf8f80f83cfbf4444d497090ed797 Mon Sep 17 00:00:00 2001 From: Tobias Macey Date: Wed, 30 Sep 2026 10:20:56 -0400 Subject: [PATCH 2/5] docs: backfill CHANGELOG.md from the release history Reconstructed from the commits and PRs between each CalVer tag, 2026.7.22.1 through 2026.9.30.1. The file has to be tracked before the first scriv release because the release resource stages with `git add -u`. Claude-Session: https://claude.ai/code/session_01VaJ4VtbQFPV63VWtQV6JEg --- CHANGELOG.md | 147 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ad7442d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,147 @@ +# Changelog + +Entries are collected from the fragments in `changelog.d/` by +[scriv](https://scriv.readthedocs.io/) when a release is cut. Entries before +the switch to scriv were reconstructed from the git history. + + + + +## 2026.9.30.1 (2026-09-30) + +### Added + +- `b2b_learner_records`: serve `/courses` from `mv_b2b_contract_courserun` ([#60](https://github.com/mitodl/ol-analytics-api/pull/60)) +- `b2b_learner_records`: serve activity from the learner-records materialized views ([#62](https://github.com/mitodl/ol-analytics-api/pull/62)) +- `b2b_learner_records`: bounded date-range filters for partitioned backfills ([#72](https://github.com/mitodl/ol-analytics-api/pull/72)) +- `b2b_dashboard`: per-status counts on the learner-progress envelope ([#68](https://github.com/mitodl/ol-analytics-api/pull/68)) +- `b2b_dashboard`: learner-progress serves activity and carries a needs-attention count ([#78](https://github.com/mitodl/ol-analytics-api/pull/78)) +- `b2b_dashboard`: filter learner-progress by course run ([#79](https://github.com/mitodl/ol-analytics-api/pull/79)) +- A per-tenant OpenAPI spec committed under `openapi/specs/`, regenerated by `bin/generate-openapi-spec` and diffed in CI ([#70](https://github.com/mitodl/ol-analytics-api/pull/70)) + +### Changed + +- Dependency and lockfile updates ([#75](https://github.com/mitodl/ol-analytics-api/pull/75), [#77](https://github.com/mitodl/ol-analytics-api/pull/77)) + +### Fixed + +- `b2b`: blank learner names are returned as `null` ([#71](https://github.com/mitodl/ol-analytics-api/pull/71)) + + +## 2026.9.17.1 (2026-09-17) + +### Added + +- `b2b_learner_records` tenant, a machine-to-machine per-learner progress API ([#56](https://github.com/mitodl/ol-analytics-api/pull/56)) +- `b2b_dashboard`: contract-scoped learner-progress endpoint ([#59](https://github.com/mitodl/ol-analytics-api/pull/59)) +- Descriptions on analytics response fields ([#57](https://github.com/mitodl/ol-analytics-api/pull/57)) +- B2B learner records spec and provider-access decision record ([#55](https://github.com/mitodl/ol-analytics-api/pull/55)) + +### Changed + +- Python base image updated to 3.14 ([#48](https://github.com/mitodl/ol-analytics-api/pull/48)) +- Dependencies pinned and updated ([#46](https://github.com/mitodl/ol-analytics-api/pull/46), [#47](https://github.com/mitodl/ol-analytics-api/pull/47), [#49](https://github.com/mitodl/ol-analytics-api/pull/49), [#50](https://github.com/mitodl/ol-analytics-api/pull/50), [#51](https://github.com/mitodl/ol-analytics-api/pull/51), [#61](https://github.com/mitodl/ol-analytics-api/pull/61)) + +### Security + +- Sentry events cap request bodies at 1KB and scrub Postgres `DETAIL` rows ([#54](https://github.com/mitodl/ol-analytics-api/pull/54)) + + +## 2026.9.3.1 (2026-09-03) + +### Changed + +- Renovate configuration ([#44](https://github.com/mitodl/ol-analytics-api/pull/44)) + +### Fixed + +- OpenTelemetry instruments the FastAPI instance rather than the module attribute ([#52](https://github.com/mitodl/ol-analytics-api/pull/52)) + + +## 2026.8.28.2 (2026-08-28) + +### Fixed + +- `b2b_dashboard`: `contract_id` is typed as an integer ([#39](https://github.com/mitodl/ol-analytics-api/pull/39)) + + +## 2026.8.28.1 (2026-08-28) + +### Added + +- `b2b_dashboard`: contract-scoped endpoints mirroring mitxonline's manager dashboard ([#34](https://github.com/mitodl/ol-analytics-api/pull/34)) + +### Changed + +- The reference Kubernetes manifest mirrors the deployed pod hardening ([#32](https://github.com/mitodl/ol-analytics-api/pull/32)) + +### Fixed + +- Stop overriding the OpenTelemetry SDK's OTLP endpoint resolution ([#35](https://github.com/mitodl/ol-analytics-api/pull/35)) + +### Security + +- `b2b_dashboard`: every activity aggregate is floored through the cohort that produced it ([#33](https://github.com/mitodl/ol-analytics-api/pull/33)) + + +## 2026.8.12.1 (2026-08-12) + +### Fixed + +- `b2b_dashboard`: contract, program, and course run primary key fields are typed as strings ([#29](https://github.com/mitodl/ol-analytics-api/pull/29)) + + +## 2026.8.3.1 (2026-08-03) + +### Changed + +- Granian is the application server ([#21](https://github.com/mitodl/ol-analytics-api/pull/21)) + +### Fixed + +- `b2b`: the org-manager check authenticates to MITx Online with OAuth2 client credentials ([#22](https://github.com/mitodl/ol-analytics-api/pull/22)) +- `b2b`: two `cohort_policy` docstrings that overstated their safety guarantees ([#23](https://github.com/mitodl/ol-analytics-api/pull/23)) + +### Security + +- zizmor analysis of GitHub Actions workflows, SHA-pinned actions, and a 7-day `exclude-newer` window on dependency resolution ([#24](https://github.com/mitodl/ol-analytics-api/pull/24)) + + +## 2026.7.30.1 (2026-07-30) + +### Added + +- `b2b`: total row count in the analytics envelopes ([#15](https://github.com/mitodl/ol-analytics-api/pull/15)) + +### Changed + +- CI: a dropped `pull_request` run can be recovered without an empty commit ([#17](https://github.com/mitodl/ol-analytics-api/pull/17)) + +### Fixed + +- `b2b`: org endpoints are keyed on the Keycloak org UUID (`sso_organization_id`) ([#13](https://github.com/mitodl/ol-analytics-api/pull/13)) + + +## 2026.7.22.1 (2026-07-22) + +First release. + +### Added + +- Multi-tenant FastAPI service serving aggregated B2B analytics from StarRocks materialized views ([#1](https://github.com/mitodl/ol-analytics-api/pull/1)) +- `as_of` + `data` envelope on analytics responses ([#7](https://github.com/mitodl/ol-analytics-api/pull/7)) +- Test coverage at 100% with coverage reporting in CI ([#8](https://github.com/mitodl/ol-analytics-api/pull/8)) +- `bump-my-version` configuration for the ol-concourse release resource + +### Changed + +- Endpoint table, tenant contract, and per-view `as_of` from the architecture review ([#6](https://github.com/mitodl/ol-analytics-api/pull/6)) + +### Fixed + +- Tenant readiness is scoped so one tenant's upstream can't fail the whole pod ([#3](https://github.com/mitodl/ol-analytics-api/pull/3)) + +### Security + +- The k-anonymity floor applies across every cohort column ([#5](https://github.com/mitodl/ol-analytics-api/pull/5)) +- StarRocks queries are bounded by `LIMIT`/pagination and query and pool timeouts ([#4](https://github.com/mitodl/ol-analytics-api/pull/4)) From 296dcc92cce9d9c62232741e6f499fbabe444ae1 Mon Sep 17 00:00:00 2001 From: Tobias Macey Date: Wed, 30 Sep 2026 10:52:38 -0400 Subject: [PATCH 3/5] fix: use the release image's uv instead of pip-installing it mitodl/ol-concourse-dsl ships uv since https://github.com/mitodl/ol-concourse/pull/111, so the hook no longer needs to install an unpinned uv from PyPI at release time. Claude-Session: https://claude.ai/code/session_01VaJ4VtbQFPV63VWtQV6JEg --- pyproject.toml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b9e8b35..b418541 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -162,14 +162,13 @@ commit = false tag = false parse = "(?P(?:[1-9][0-9]{3})\\.(?:1[0-2]|[1-9])\\.(?:3[0-1]|[12][0-9]|[1-9]))\\.(?P\\d+)" serialize = ["{release}.{build}"] -# The ol-concourse release job runs `bump-my-version bump --no-commit` in an -# image that has pip but neither uv nor scriv, then the release resource commits -# the tree with `git add -u`. Pre-commit hooks still run under --no-commit, after -# pyproject.toml carries the new version, so scriv reads it from there. -# `git add -u` stages only tracked files, so CHANGELOG.md has to exist on main -# before a release rather than be created by one. +# The ol-concourse release job runs `bump-my-version bump --no-commit` in the +# mitodl/ol-concourse-dsl image, which ships uv but not scriv, then the release +# resource commits the tree with `git add -u`. Pre-commit hooks still run under +# --no-commit, after pyproject.toml carries the new version, so scriv reads it +# from there. `git add -u` stages only tracked files, so CHANGELOG.md has to +# exist on main before a release rather than be created by one. pre_commit_hooks = [ - "python3 -m pip install --quiet --root-user-action=ignore uv", "uv run --frozen --only-group release bin/collect-changelog", ] From 8944a40eddbf3fb22a7dab48caa7ae2584dae9b1 Mon Sep 17 00:00:00 2001 From: Tobias Macey Date: Wed, 30 Sep 2026 11:04:26 -0400 Subject: [PATCH 4/5] fix: use scriv's current changelog setting and reword the fragment `output_file` is a deprecated alias for `changelog` in scriv 1.8.0. The fragment claimed every PR adds one, which nothing enforces. Claude-Session: https://claude.ai/code/session_01VaJ4VtbQFPV63VWtQV6JEg --- changelog.d/20260930_101301_blarghmatey_scriv_changelog.md | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog.d/20260930_101301_blarghmatey_scriv_changelog.md b/changelog.d/20260930_101301_blarghmatey_scriv_changelog.md index cbd3aa8..f5c8d2c 100644 --- a/changelog.d/20260930_101301_blarghmatey_scriv_changelog.md +++ b/changelog.d/20260930_101301_blarghmatey_scriv_changelog.md @@ -1,3 +1,3 @@ ### Added -- Changelog fragments with scriv. Each PR adds a fragment under `changelog.d/`, and the release job collects them into `CHANGELOG.md` during the version bump. +- Changelog fragments with scriv. A change worth noting adds a fragment under `changelog.d/`, and the release job collects whatever fragments are present into `CHANGELOG.md` during the version bump. diff --git a/pyproject.toml b/pyproject.toml index b418541..5e0c8d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -150,7 +150,7 @@ ignore_missing_imports = true [tool.scriv] format = "md" fragment_directory = "changelog.d" -output_file = "CHANGELOG.md" +changelog = "CHANGELOG.md" version = "literal: pyproject.toml: project.version" categories = ["Added", "Changed", "Deprecated", "Removed", "Fixed", "Security"] md_header_level = "2" From 3254426df332caa97f73cdbfedd00e7ea8a6ce59 Mon Sep 17 00:00:00 2001 From: Tobias Macey Date: Wed, 30 Sep 2026 11:04:26 -0400 Subject: [PATCH 5/5] test: cover both branches of bin/collect-changelog The release hook fails the release if this script fails, so run it against the repo's real scriv config with and without fragments. Claude-Session: https://claude.ai/code/session_01VaJ4VtbQFPV63VWtQV6JEg --- tests/test_collect_changelog.py | 64 +++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 tests/test_collect_changelog.py diff --git a/tests/test_collect_changelog.py b/tests/test_collect_changelog.py new file mode 100644 index 0000000..627a4fe --- /dev/null +++ b/tests/test_collect_changelog.py @@ -0,0 +1,64 @@ +"""bin/collect-changelog, run against a copy of the repo's real scriv config. + +The release job runs this from bump-my-version's pre_commit_hooks, so a failure +here fails the release. Both branches matter: a release with fragments has to +fold them into CHANGELOG.md, and a release with none (e.g. only Renovate +updates) has to succeed without touching it. +""" + +import shutil +import subprocess +import sys +from pathlib import Path + +import pytest + +REPO_ROOT = Path(__file__).resolve().parent.parent +SCRIPT = REPO_ROOT / "bin" / "collect-changelog" +CHANGELOG_HEADER = "# Changelog\n\n\n" + + +@pytest.fixture +def project(tmp_path: Path) -> Path: + shutil.copy(REPO_ROOT / "pyproject.toml", tmp_path / "pyproject.toml") + (tmp_path / "CHANGELOG.md").write_text(CHANGELOG_HEADER) + (tmp_path / "changelog.d").mkdir() + (tmp_path / "changelog.d" / ".gitkeep").touch() + return tmp_path + + +def run_collect(cwd: Path) -> subprocess.CompletedProcess[str]: + return subprocess.run( # noqa: S603 + [sys.executable, str(SCRIPT)], + cwd=cwd, + capture_output=True, + text=True, + check=False, + ) + + +def test_collects_fragments_under_the_project_version(project): + fragment = project / "changelog.d" / "20260101_000000_someone_change.md" + fragment.write_text("### Fixed\n\n- A fixed thing.\n") + + result = run_collect(project) + + assert result.returncode == 0, result.stderr + changelog = (project / "CHANGELOG.md").read_text() + version = next( + line.split('"')[1] + for line in (project / "pyproject.toml").read_text().splitlines() + if line.startswith("version = ") + ) + assert f"## {version} (" in changelog + assert "### Fixed\n\n- A fixed thing." in changelog + assert not fragment.exists() + assert (project / "changelog.d" / ".gitkeep").exists() + + +def test_no_fragments_is_a_no_op(project): + result = run_collect(project) + + assert result.returncode == 0, result.stderr + assert "No changelog fragments to collect" in result.stdout + assert (project / "CHANGELOG.md").read_text() == CHANGELOG_HEADER