Skip to content

Commit 6a19a19

Browse files
build(medcat-deps): CU-869c482nb Update scripts and tutorials automatically (#333)
* CU-869c482nb: Add script to bump dependency versions * CU-869c482nb: Add new workflow that updates the medcat-script and medcat-v2-tutorial deps automatically --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent a806334 commit 6a19a19

File tree

2 files changed

+207
-0
lines changed

2 files changed

+207
-0
lines changed

.github/scripts/bump_dependants.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
# .github/scripts/bump_dependants.py
2+
"""
3+
Bumps the core package version in all dependant projects and opens a PR for each.
4+
Handles both direct and optional/extra dependencies in requirements.txt and pyproject.toml.
5+
Only updates entries using == or ~= specifiers (leaves unpinned or ranged deps alone).
6+
"""
7+
8+
import argparse
9+
import re
10+
import os
11+
import subprocess
12+
import sys
13+
from pathlib import Path
14+
15+
SUPPORTED_SPECIFIERS = ("==", "~=")
16+
17+
18+
def parse_args():
19+
parser = argparse.ArgumentParser()
20+
parser.add_argument("--version", required=True, help="New version, e.g. 1.5.0")
21+
parser.add_argument("--package", required=True, help="Core package name")
22+
parser.add_argument("--dependants", nargs="+", required=True, help="Project directories to update")
23+
return parser.parse_args()
24+
25+
26+
def bump_requirements_txt(path: Path, package: str, new_version: str) -> bool:
27+
"""
28+
Matches lines like:
29+
package==1.2.3
30+
package~=1.2
31+
package[extra1,extra2]==1.2.3
32+
package[extra1,extra2]~=1.2
33+
Leaves unpinned or range-pinned (>=, <=, !=) entries untouched.
34+
"""
35+
pattern = re.compile(
36+
rf"^({re.escape(package)}(?:\[[^\]]*\])?)({'|'.join(re.escape(s) for s in SUPPORTED_SPECIFIERS)})[^\s#]+",
37+
re.MULTILINE,
38+
)
39+
original = path.read_text()
40+
updated, count = pattern.subn(rf"\g<1>~={new_version}", original)
41+
if count:
42+
path.write_text(updated)
43+
return bool(count)
44+
45+
46+
def bump_pyproject_toml(path: Path, package: str, new_version: str) -> bool:
47+
"""
48+
Matches PEP 508 strings in pyproject.toml, covering:
49+
- [project] dependencies
50+
- [project.optional-dependencies] groups
51+
- [tool.poetry.dependencies] and similar
52+
e.g. 'your-core-library==1.2.3', 'your-core-library[opt]~=1.2'
53+
Uses regex rather than a TOML parser to preserve file formatting.
54+
"""
55+
pattern = re.compile(
56+
rf'({re.escape(package)}(?:\[[^\]]*\])?)({"|".join(re.escape(s) for s in SUPPORTED_SPECIFIERS)})[^\s,"\']+',
57+
)
58+
original = path.read_text()
59+
updated, count = pattern.subn(rf"\g<1>~={new_version}", original)
60+
if count:
61+
path.write_text(updated)
62+
return bool(count)
63+
64+
65+
def bump_project(project_dir: Path, package: str, new_version: str) -> list[Path]:
66+
"""Returns list of files that were modified."""
67+
modified = []
68+
69+
candidates = {
70+
"requirements.txt": bump_requirements_txt,
71+
"requirements-dev.txt": bump_requirements_txt,
72+
"pyproject.toml": bump_pyproject_toml,
73+
}
74+
75+
for filename, bump_fn in candidates.items():
76+
fpath = project_dir / filename
77+
if fpath.exists() and bump_fn(fpath, package, new_version):
78+
modified.append(fpath)
79+
80+
return modified
81+
82+
83+
def run(cmd: list[str], **kwargs) -> subprocess.CompletedProcess:
84+
print(f" $ {' '.join(cmd)}")
85+
return subprocess.run(cmd, check=True, **kwargs)
86+
87+
88+
def open_pr(branch: str, title: str, body: str, base: str = "main"):
89+
# Guard against a PR already being open for this branch
90+
result = subprocess.run(
91+
["gh", "pr", "list", "--head", branch, "--json", "number"],
92+
capture_output=True, text=True, check=True,
93+
)
94+
if '"number"' in result.stdout:
95+
print(f" PR already open for {branch}, skipping")
96+
return
97+
98+
run(["gh", "pr", "create",
99+
"--title", title,
100+
"--body", body,
101+
"--base", base,
102+
"--head", branch,
103+
"--label", "dependencies"])
104+
105+
106+
def main():
107+
args = parse_args()
108+
package = args.package
109+
new_version = args.version
110+
repo_root = Path(__file__).resolve().parents[2]
111+
112+
run(["git", "config", "user.name", "github-actions[bot]"])
113+
run(["git", "config", "user.email", "github-actions[bot]@users.noreply.github.com"])
114+
115+
for dependant in args.dependants:
116+
dependant = dependant.strip().removesuffix(os.path.sep).strip()
117+
print(f"\nProcessing: {dependant}")
118+
project_dir = repo_root / dependant
119+
if not project_dir.is_dir():
120+
print(f" Directory not found, skipping")
121+
continue
122+
123+
modified = bump_project(project_dir, package, new_version)
124+
if not modified:
125+
print(f" No pinned {package} dependency found, skipping")
126+
continue
127+
128+
branch = f"chore/bump-{package}-{new_version}-in-{dependant}"
129+
run(["git", "checkout", "-b", branch])
130+
131+
for fpath in modified:
132+
run(["git", "add", str(fpath)])
133+
134+
run(["git", "commit", "-m", f"chore({dependant}): bump {package} to {new_version}"])
135+
run(["git", "push", "origin", branch])
136+
137+
open_pr(
138+
branch=branch,
139+
title=f"chore({dependant}): bump {package} to ~={new_version}",
140+
body=(
141+
f"Automated minor version bump of `{package}` to `~={new_version}` "
142+
f"following the upstream release.\n\n"
143+
f"Files updated:\n"
144+
+ "\n".join(f"- `{f.relative_to(repo_root)}`" for f in modified)
145+
),
146+
)
147+
148+
run(["git", "checkout", "main"])
149+
150+
print("\nDone.")
151+
152+
153+
if __name__ == "__main__":
154+
sys.exit(main())
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
name: Bump dependants on release
2+
3+
on:
4+
push:
5+
tags:
6+
- 'medcat/v*'
7+
- 'MedCAT/v*'
8+
9+
jobs:
10+
bump-dependants:
11+
runs-on: ubuntu-latest
12+
permissions:
13+
contents: write
14+
pull-requests: write
15+
16+
steps:
17+
- name: Checkout repo
18+
uses: actions/checkout@v4
19+
20+
- name: Extract version from tag
21+
id: version
22+
run: |
23+
TAG="${GITHUB_REF_NAME}"
24+
VERSION="${TAG#medcat/}"
25+
VERSION="${VERSION#MedCAT/}"
26+
echo "version=$VERSION" >> $GITHUB_OUTPUT
27+
28+
- name: Check if minor release
29+
id: release_type
30+
run: |
31+
VERSION="${{ steps.version.outputs.version }}"
32+
PATCH=$(echo "$VERSION" | cut -d. -f3)
33+
if [ "$PATCH" = "0" ]; then
34+
echo "is_minor=true" >> $GITHUB_OUTPUT
35+
else
36+
echo "is_minor=false" >> $GITHUB_OUTPUT
37+
fi
38+
39+
- name: Set up Python
40+
if: steps.release_type.outputs.is_minor == 'true'
41+
uses: actions/setup-python@v5
42+
with:
43+
python-version: '3.11'
44+
45+
- name: Bump dependants
46+
if: steps.release_type.outputs.is_minor == 'true'
47+
env:
48+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
49+
run: |
50+
python .github/scripts/bump_dependants.py \
51+
--version "${{ steps.version.outputs.version }}" \
52+
--package "medcat" \
53+
--dependants "medcat-scripts" "medcat-v2-tutorials"

0 commit comments

Comments
 (0)