Skip to content

Commit 94b2304

Browse files
(chore): automation of towncrier release process (#1626)
Co-authored-by: Philipp A. <flying-sheep@web.de>
1 parent dde809d commit 94b2304

5 files changed

Lines changed: 130 additions & 1 deletion

File tree

ci/scripts/towncrier_automation.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
#!python3
2+
from __future__ import annotations
3+
4+
import argparse
5+
import subprocess
6+
from typing import TYPE_CHECKING
7+
8+
from packaging import version
9+
10+
if TYPE_CHECKING:
11+
from collections.abc import Sequence
12+
13+
14+
class Args(argparse.Namespace):
15+
version: str
16+
dry_run: bool
17+
18+
19+
def parse_args(argv: Sequence[str] | None = None) -> Args:
20+
parser = argparse.ArgumentParser(
21+
prog="towncrier-automation",
22+
description=(
23+
"This script runs towncrier for a given version, "
24+
"creates a branch off of the current one, "
25+
"and then creates a PR into the original branch with the changes. "
26+
"The PR will be backported to main if the current branch is not main."
27+
),
28+
)
29+
parser.add_argument(
30+
"version",
31+
type=str,
32+
help=(
33+
"The new version for the release must have at least three parts, like `major.minor.patch` and no `major.minor`. "
34+
"It can have a suffix like `major.minor.patch.dev0` or `major.minor.0rc1`."
35+
),
36+
)
37+
parser.add_argument(
38+
"--dry-run",
39+
help="Whether or not to dry-run the actual creation of the pull request",
40+
action="store_true",
41+
)
42+
args = parser.parse_args(argv, Args())
43+
if len(version.Version(args.version).release) != 3:
44+
raise ValueError(
45+
f"Version argument {args.version} must contain major, minor, and patch version."
46+
)
47+
version.parse(args.version) # validate
48+
return args
49+
50+
51+
def main(argv: Sequence[str] | None = None) -> None:
52+
args = parse_args(argv)
53+
54+
# Run towncrier
55+
subprocess.run(
56+
["towncrier", "build", f"--version={args.version}", "--yes"], check=True
57+
)
58+
59+
# Check if we are on the main branch to know if we need to backport
60+
base_branch = subprocess.run(
61+
["git", "rev-parse", "--abbrev-ref", "HEAD"],
62+
capture_output=True,
63+
text=True,
64+
check=True,
65+
).stdout.strip()
66+
pr_description = "" if base_branch == "main" else "on-merge: backport to main"
67+
branch_name = f"release_notes_{args.version}"
68+
69+
# Create a new branch + commit
70+
subprocess.run(["git", "switch", "-c", branch_name], check=True)
71+
subprocess.run(["git", "add", "docs/release-notes"], check=True)
72+
pr_title = f"(chore): generate {args.version} release notes"
73+
subprocess.run(["git", "commit", "-m", pr_title], check=True)
74+
75+
# Create a PR
76+
subprocess.run(
77+
[
78+
"gh",
79+
"pr",
80+
"create",
81+
f"--base={base_branch}",
82+
f"--head={branch_name}",
83+
f"--title={pr_title}",
84+
f"--body={pr_description}",
85+
*(["--dry-run"] if args.dry_run else []),
86+
],
87+
check=True,
88+
)
89+
90+
# Enable auto-merge
91+
if not args.dry_run:
92+
subprocess.run(
93+
["gh", "pr", "merge", branch_name, "--auto", "--squash"], check=True
94+
)
95+
else:
96+
print("Dry run, not merging")
97+
98+
99+
if __name__ == "__main__":
100+
main()

docs/contributing.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,21 @@ AnnData follows the development practices outlined in the [Scanpy contribution g
66
.. include:: _key_contributors.rst
77
```
88

9+
## Release Notes
10+
11+
AnnData differs from `scanpy` (for now) in how its releases are done.
12+
It uses [towncrier][] to build its changelog.
13+
We have set up some automation around this process.
14+
To run `towncrier`, create a `PR` into the base branch of the release with the compiled changelog, and backport to `main` if needed (i.e., the base branch is something like `0.10.x`), run
15+
16+
```shell
17+
hatch run towncrier:build X.Y.Z
18+
```
19+
20+
You may add the option `--dry-run` at the end to do the local steps without pushing to Github, although the push will be mocked via [`gh pr --dry-run`](https://cli.github.com/manual/gh_pr_create).
21+
22+
[towncrier]: https://towncrier.readthedocs.io/en/stable/
23+
924
## CI
1025

1126
### GPU CI

hatch.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,7 @@ dependencies = ["setuptools"] # https://bitbucket.org/pybtex-devs/pybtex/issues
99
[envs.docs.scripts]
1010
build = "sphinx-build -M html docs docs/_build -W --keep-going {args}"
1111
clean = "git clean -fX -- docs"
12+
13+
[envs.towncrier.scripts]
14+
build = "python3 ci/scripts/towncrier_automation.py {args}"
15+
clean = "git restore --source=HEAD --staged --worktree -- docs/release-notes"

pyproject.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,9 +170,11 @@ select = [
170170
"E", # Error detected by Pycodestyle
171171
"F", # Errors detected by Pyflakes
172172
"W", # Warning detected by Pycodestyle
173+
"PLW", # Pylint
173174
"UP", # pyupgrade
174175
"I", # isort
175176
"TCH", # manage type checking blocks
177+
"TID", # Banned imports
176178
"ICN", # Follow import conventions
177179
"PTH", # Pathlib instead of os.path
178180
"PT", # Pytest conventions
@@ -184,13 +186,21 @@ ignore = [
184186
"E731",
185187
# allow I, O, l as variable names -> I is the identity matrix, i, j, k, l is reasonable indexing notation
186188
"E741",
189+
# We use relative imports from parent modules
190+
"TID252",
191+
# Shadowing loop variables isn’t a big deal
192+
"PLW2901",
187193
]
188194
[tool.ruff.lint.per-file-ignores]
189195
# E721 comparing types, but we specifically are checking that we aren't getting subtypes (views)
190196
"tests/test_readwrite.py" = ["E721"]
191197
[tool.ruff.lint.isort]
192198
known-first-party = ["anndata"]
193199
required-imports = ["from __future__ import annotations"]
200+
[tool.ruff.lint.flake8-tidy-imports.banned-api]
201+
"subprocess.call".msg = "Use `subprocess.run([…])` instead"
202+
"subprocess.check_call".msg = "Use `subprocess.run([…], check=True)` instead"
203+
"subprocess.check_output".msg = "Use `subprocess.run([…], check=True, capture_output=True)` instead"
194204
[tool.ruff.lint.flake8-type-checking]
195205
exempt-modules = []
196206
strict = true

src/anndata/logging.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def get_memory_usage():
3131
meminfo = process.get_memory_info()
3232
mem = meminfo[0] / 2**30 # output in GB
3333
mem_diff = mem
34-
global _previous_memory_usage
34+
global _previous_memory_usage # noqa: PLW0603
3535
if _previous_memory_usage is not None:
3636
mem_diff = mem - _previous_memory_usage
3737
_previous_memory_usage = mem

0 commit comments

Comments
 (0)