Skip to content

Commit 7531de5

Browse files
authored
chore(zarr-metadata): set up a changelog (zarr-developers#3981)
* chore: set up changelog * chore: update towncrier CI check to handle subpackages, and improve its behavior
1 parent 7afdc8d commit 7531de5

5 files changed

Lines changed: 140 additions & 15 deletions

File tree

.github/workflows/check_changelogs.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,8 @@ jobs:
2424
- name: Install uv
2525
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
2626

27-
- name: Check changelog entries
27+
- name: Check zarr-python changelog entries
2828
run: uv run --no-sync python ci/check_changelog_entries.py
29+
30+
- name: Check zarr-metadata changelog entries
31+
run: uv run --no-sync python ci/check_changelog_entries.py packages/zarr-metadata/changes

ci/check_changelog_entries.py

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
"""
22
Check changelog entries have the correct filename structure.
3+
4+
Usage:
5+
python check_changelog_entries.py [DIRECTORY]
6+
7+
DIRECTORY defaults to the repo-root `changes/`.
38
"""
49

510
import sys
611
from pathlib import Path
712

813
VALID_CHANGELOG_TYPES = ["feature", "bugfix", "doc", "removal", "misc"]
9-
CHANGELOG_DIRECTORY = (Path(__file__).parent.parent / "changes").resolve()
14+
REPO_ROOT = Path(__file__).parent.parent.resolve()
15+
DEFAULT_DIRECTORY = REPO_ROOT / "changes"
1016

1117

1218
def is_int(s: str) -> bool:
@@ -18,34 +24,47 @@ def is_int(s: str) -> bool:
1824
return True
1925

2026

21-
if __name__ == "__main__":
22-
print(f"Looking for changelog entries in {CHANGELOG_DIRECTORY}")
23-
entries = CHANGELOG_DIRECTORY.glob("*")
27+
def check(directory: Path) -> int:
28+
print(f"Looking for changelog entries in {directory}")
29+
entries = list(directory.glob("*"))
2430
entries = [e for e in entries if e.name not in [".gitignore", "README.md"]]
2531
print(f"Found {len(entries)} entries")
2632
print()
2733

2834
bad_suffix = [e for e in entries if e.suffix != ".md"]
2935
bad_issue_no = [e for e in entries if not is_int(e.name.split(".")[0])]
30-
bad_type = [e for e in entries if e.name.split(".")[1] not in VALID_CHANGELOG_TYPES]
36+
# Only flag bad_type for files that have already passed the prior two
37+
# checks; otherwise `e.name.split(".")[1]` may raise IndexError on a
38+
# malformed name like `notes.md`.
39+
bad_type = [
40+
e
41+
for e in entries
42+
if e.suffix == ".md"
43+
and is_int(e.name.split(".")[0])
44+
and e.name.split(".")[1] not in VALID_CHANGELOG_TYPES
45+
]
3146

32-
if len(bad_suffix) or len(bad_issue_no) or len(bad_type):
33-
if len(bad_suffix):
47+
if bad_suffix or bad_issue_no or bad_type:
48+
if bad_suffix:
3449
print("Changelog entries without .md suffix")
3550
print("-------------------------------------")
36-
print("\n".join([p.name for p in bad_suffix]))
51+
print("\n".join(p.name for p in bad_suffix))
3752
print()
38-
if len(bad_issue_no):
53+
if bad_issue_no:
3954
print("Changelog entries without integer issue number")
4055
print("----------------------------------------------")
41-
print("\n".join([p.name for p in bad_issue_no]))
56+
print("\n".join(p.name for p in bad_issue_no))
4257
print()
43-
if len(bad_type):
58+
if bad_type:
4459
print("Changelog entries without valid type")
4560
print("------------------------------------")
46-
print("\n".join([p.name for p in bad_type]))
61+
print("\n".join(p.name for p in bad_type))
4762
print(f"Valid types are: {VALID_CHANGELOG_TYPES}")
4863
print()
49-
sys.exit(1)
64+
return 1
65+
return 0
5066

51-
sys.exit(0)
67+
68+
if __name__ == "__main__":
69+
directory = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else DEFAULT_DIRECTORY
70+
sys.exit(check(directory))
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Release notes
2+
3+
<!-- towncrier release notes start -->
4+
5+
## 0.2.0 (2026-05-11)
6+
7+
### Bugfixes
8+
9+
- `GzipCodecConfiguration.level` is now required, and `GzipCodecMetadata`
10+
no longer accepts the bare-string `"gzip"` form. The codec's compressed
11+
output depends on `level`, so metadata that omits it cannot reproducibly
12+
identify the chunk bytes produced by a writer. **Breaking** for consumers
13+
that previously typed gzip codec metadata as the bare string or
14+
constructed a `GzipCodecConfiguration` without `level`.
15+
([#3978](https://github.com/zarr-developers/zarr-python/issues/3978))
16+
- `BytesCodecObject.configuration` is now `NotRequired`. The configuration
17+
has no required keys (`endian` is conditionally required at runtime
18+
based on data type), so the object form may omit it entirely — matching
19+
the bare-string short-hand. **Soft-breaking** for consumers that
20+
previously relied on `configuration` always being present.
21+
([#3978](https://github.com/zarr-developers/zarr-python/issues/3978))
22+
- Better modelling of Zarr v2 stored metadata. Zarr v2 splits a node's
23+
metadata across two JSON documents (`.zarray`/`.zgroup` and `.zattrs`),
24+
but `GroupMetadataV2` had no `attributes` field while `ArrayMetadataV2`
25+
did — an inconsistency. `GroupMetadataV2` now also has an optional
26+
`attributes` field, and `ArrayMetadataV2.attributes` is now
27+
`NotRequired` for symmetry. **Soft-breaking** for consumers that
28+
relied on `ArrayMetadataV2.attributes` always being present.
29+
([#3962](https://github.com/zarr-developers/zarr-python/issues/3962))
30+
31+
### Features
32+
33+
- Added three new top-level types modelling the **strict on-disk** shape
34+
of Zarr v2 metadata documents: `ZArrayMetadata` (the `.zarray` file),
35+
`ZGroupMetadata` (the `.zgroup` file), and `ZAttrsMetadata` (the
36+
`.zattrs` file). Use these when you want a type that faithfully matches
37+
what's stored on disk; use the merged `ArrayMetadataV2`/`GroupMetadataV2`
38+
when you want the in-memory representation a Python program typically
39+
works with.
40+
([#3962](https://github.com/zarr-developers/zarr-python/issues/3962))
41+
- Added typed constants exposing the spec-permitted values of constrained
42+
Literal fields, importable at the per-codec module level. For example,
43+
`from zarr_metadata.v3.codec.bytes import ENDIAN` provides
44+
`("little", "big")` as a tuple, enabling runtime iteration or validator
45+
generation without re-stating the Literal values by hand.
46+
([#3978](https://github.com/zarr-developers/zarr-python/issues/3978))
47+
48+
## 0.1.1 (2026-05-06)
49+
50+
### Misc
51+
52+
- First usable release on PyPI. Version 0.1.0 was uploaded then deleted to
53+
reserve the project name; this version is the first one PyPI will install.
54+
No source changes from 0.1.0.
55+
([#3949](https://github.com/zarr-developers/zarr-python/issues/3949))
56+
57+
## 0.1.0 (2026-05-01)
58+
59+
### Features
60+
61+
- Initial release. Provides `TypedDict` definitions and `Literal` aliases
62+
for the JSON shapes specified by Zarr v2 and v3 metadata, plus a subset
63+
of `zarr-extensions` types and the un-specified-but-widely-used
64+
consolidated metadata documents. Pair with a runtime validator like
65+
`pydantic` to check JSON loaded from disk.
66+
([#3919](https://github.com/zarr-developers/zarr-python/issues/3919))
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
Writing a changelog entry for `zarr-metadata`
2+
---------------------------------------------
3+
4+
Fragments in **this** directory are released notes for the `zarr-metadata`
5+
package only — kept separate from the parent zarr-python `changes/`
6+
directory so a PR touching only `packages/zarr-metadata/` produces a
7+
release note for this package only.
8+
9+
Please put a new file in this directory named `xxxx.<type>.md`, where
10+
11+
- `xxxx` is the pull request number associated with this entry
12+
- `<type>` is one of:
13+
- feature
14+
- bugfix
15+
- doc
16+
- removal
17+
- misc
18+
19+
Inside the file, please write a short description of what you have
20+
changed, and how it impacts users of `zarr-metadata`.
21+
22+
A `zarr-metadata` release runs `towncrier build` in `packages/zarr-metadata/`,
23+
which consumes the fragments here and updates `CHANGELOG.md`. Fragments
24+
that describe parent zarr-python changes (not the metadata package)
25+
belong in the top-level `changes/` directory, not here.

packages/zarr-metadata/pyproject.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,15 @@ include = ["src"]
7474
enableExperimentalFeatures = true
7575
typeCheckingMode = "strict"
7676
pythonVersion = "3.11"
77+
78+
[tool.towncrier]
79+
# Fragments for this package live alongside the package source, separate
80+
# from the parent zarr-python `changes/` directory, so a PR touching only
81+
# `packages/zarr-metadata/` produces a release note for this package only.
82+
directory = "changes"
83+
filename = "CHANGELOG.md"
84+
package = "zarr_metadata"
85+
underlines = ["", "", ""]
86+
title_format = "## {version} ({project_date})"
87+
issue_format = "[#{issue}](https://github.com/zarr-developers/zarr-python/issues/{issue})"
88+
start_string = "<!-- towncrier release notes start -->\n"

0 commit comments

Comments
 (0)