Skip to content

Commit 09a9ecd

Browse files
Merge branch 'main' into develop
# Conflicts: # requirements/env_climada.yml
2 parents 5d54dba + 47b80ad commit 09a9ecd

File tree

7 files changed

+512
-0
lines changed

7 files changed

+512
-0
lines changed

.github/scripts/make_release.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#!/usr/bin/env python3
2+
"""This script is part of the GitHub CI make-release pipeline
3+
4+
It reads the version number from climada/_version.py and then uses the `gh` cli
5+
to create the new release.
6+
7+
"""
8+
import glob
9+
import re
10+
import subprocess
11+
12+
13+
def get_version() -> str:
14+
"""Return the current version number, based on the _version.py file."""
15+
[version_file] = glob.glob("climada*/_version.py")
16+
with open(version_file, 'r', encoding="UTF-8") as vfp:
17+
content = vfp.read()
18+
regex = r'^__version__\s*=\s*[\'\"](.*)[\'\"]\s*$'
19+
mtch = re.match(regex, content)
20+
return mtch.group(1)
21+
22+
23+
def make_release():
24+
"""run `gh release create vX.Y.Z"""
25+
version_number = get_version()
26+
subprocess.run(
27+
["gh", "release", "create", "--generate-notes", f"v{version_number}"],
28+
check=True,
29+
)
30+
31+
32+
if __name__ == "__main__":
33+
make_release()

.github/scripts/prepare_release.py

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
#!/usr/bin/env python3
2+
"""This script is part of the GitHub CI make-release pipeline
3+
4+
The following preparation steps are executed:
5+
6+
- update version numbers in _version.py and setup.py
7+
- purge the "Unreleased" section of CHANGELOG.md and rename it to the new version number
8+
- copy the README.md file to doc/misc/README.md,
9+
but without the badges as they interfere with the sphinx doc builder
10+
11+
All changes are immediately commited to the repository.
12+
"""
13+
14+
import glob
15+
import json
16+
import re
17+
import subprocess
18+
import time
19+
20+
21+
def get_last_version() -> str:
22+
"""Return the version number of the last release."""
23+
json_string = (
24+
subprocess.run(
25+
["gh", "release", "view", "--json", "tagName"],
26+
check=True,
27+
stdout=subprocess.PIPE,
28+
stderr=subprocess.PIPE,
29+
)
30+
.stdout.decode("utf8")
31+
.strip()
32+
)
33+
34+
return json.loads(json_string)["tagName"]
35+
36+
37+
def bump_version_number(version_number: str, level: str) -> str:
38+
"""Return a copy of `version_number` with one level number incremented."""
39+
major, minor, patch = version_number.split(".")
40+
if level == "major":
41+
major = str(int(major)+1)
42+
elif level == "minor":
43+
minor = str(int(minor)+1)
44+
elif level == "patch":
45+
patch = str(int(patch)+1)
46+
else:
47+
raise ValueError(f"level should be 'major', 'minor' or 'patch', not {level}")
48+
return ".".join([major, minor, patch])
49+
50+
51+
def update_readme(_nvn):
52+
"""align doc/misc/README.md with ./README.md but remove the non-markdown header lines from """
53+
with open("README.md", 'r', encoding="UTF-8") as rmin:
54+
lines = [line for line in rmin.readlines() if not line.startswith('[![')]
55+
while not lines[0].strip():
56+
lines = lines[1:]
57+
with open("doc/misc/README.md", 'w', encoding="UTF-8") as rmout:
58+
rmout.writelines(lines)
59+
return GitFile('doc/misc/README.md')
60+
61+
62+
def update_changelog(nvn):
63+
"""Rename the "Unreleased" section, remove unused subsections and the code-freeze date,
64+
set the release date to today"""
65+
releases = []
66+
release_name = None
67+
release = []
68+
section_name = None
69+
section = []
70+
with open("CHANGELOG.md", 'r', encoding="UTF-8") as changelog:
71+
for line in changelog.readlines():
72+
if line.startswith('#'):
73+
if line.startswith('### '):
74+
if section:
75+
release.append((section_name, section))
76+
section_name = line[4:].strip()
77+
section = []
78+
#print("tag:", section_name)
79+
elif line.startswith('## '):
80+
if section:
81+
release.append((section_name, section))
82+
if release:
83+
releases.append((release_name, release))
84+
release_name = line[3:].strip()
85+
release = []
86+
section_name = None
87+
section = []
88+
#print("release:", release_name)
89+
else:
90+
section.append(line)
91+
if section:
92+
release.append((section_name, section))
93+
if release:
94+
releases.append((release_name, release))
95+
96+
with open("CHANGELOG.md", 'w', encoding="UTF-8") as changelog:
97+
changelog.write("# Changelog\n\n")
98+
for release_name, release in releases:
99+
if release_name:
100+
if release_name.lower() == "unreleased":
101+
release_name = nvn
102+
changelog.write(f"## {release_name}\n")
103+
for section_name, section in release:
104+
if any(ln.strip() for ln in section):
105+
if section_name:
106+
changelog.write(f"### {section_name}\n")
107+
lines = [ln.strip() for ln in section if "code freeze date: " not in ln.lower()]
108+
if not section_name and release_name.lower() == nvn:
109+
print("setting date")
110+
for i, line in enumerate(lines):
111+
if "release date: " in line.lower():
112+
today = time.strftime("%Y-%m-%d")
113+
lines[i] = f"Release date: {today}"
114+
changelog.write("\n".join(lines).replace("\n\n", "\n"))
115+
changelog.write("\n")
116+
return GitFile('CHANGELOG.md')
117+
118+
119+
def update_version(nvn):
120+
"""Update the _version.py file"""
121+
[file_with_version] = glob.glob("climada*/_version.py")
122+
regex = r'(^__version__\s*=\s*[\'\"]).*([\'\"]\s*$)'
123+
return update_file(file_with_version, regex, nvn)
124+
125+
126+
def update_setup(new_version_number):
127+
"""Update the setup.py file"""
128+
file_with_version = "setup.py"
129+
regex = r'(^\s+version\s*=\s*[\'\"]).*([\'\"]\s*,\s*$)'
130+
return update_file(file_with_version, regex, new_version_number)
131+
132+
133+
def update_file(file_with_version, regex, new_version_number):
134+
"""Replace the version number(s) in a file, based on a rgular expression."""
135+
with open(file_with_version, 'r', encoding="UTF-8") as curf:
136+
lines = curf.readlines()
137+
successfully_updated = False
138+
for i, line in enumerate(lines):
139+
mtch = re.match(regex, line)
140+
if mtch:
141+
lines[i] = f"{mtch.group(1)}{new_version_number}{mtch.group(2)}"
142+
successfully_updated = True
143+
if not successfully_updated:
144+
raise RuntimeError(f"cannot determine version of {file_with_version}")
145+
with open(file_with_version, 'w', encoding="UTF-8") as newf:
146+
for line in lines:
147+
newf.write(line)
148+
return GitFile(file_with_version)
149+
150+
151+
class GitFile():
152+
"""Helper class for `git add`."""
153+
def __init__(self, path):
154+
self.path = path
155+
156+
def gitadd(self):
157+
"""run `git add`"""
158+
_gitadd = subprocess.run(
159+
["git", "add", self.path],
160+
check=True,
161+
stdout=subprocess.PIPE,
162+
stderr=subprocess.PIPE,
163+
).stdout.decode("utf8")
164+
165+
166+
class Git():
167+
"""Helper class for `git commit`."""
168+
def __init__(self):
169+
_gitname = subprocess.run(
170+
["git", "config", "--global", "user.name", "'climada'"],
171+
check=True,
172+
stdout=subprocess.PIPE,
173+
stderr=subprocess.PIPE,
174+
).stdout.decode("utf8")
175+
_gitemail = subprocess.run(
176+
["git", "config", "--global", "user.email", "'[email protected]'"],
177+
check=True,
178+
stdout=subprocess.PIPE,
179+
stderr=subprocess.PIPE,
180+
).stdout.decode("utf8")
181+
182+
def commit(self, new_version):
183+
"""run `git commit`."""
184+
try:
185+
_gitcommit = subprocess.run(
186+
["git", "commit", "-m", f"'Automated update v{new_version}'"],
187+
check=True,
188+
stdout=subprocess.PIPE,
189+
stderr=subprocess.PIPE,
190+
).stdout.decode("utf8")
191+
_gitpush = subprocess.run(
192+
["git", "push"],
193+
check=True,
194+
stdout=subprocess.PIPE,
195+
stderr=subprocess.PIPE,
196+
).stdout.decode("utf8")
197+
except subprocess.CalledProcessError as err:
198+
message = err.stdout.decode("utf8")
199+
print("message:", message)
200+
if "nothing to commit" in message:
201+
print("repo already up to date with new version number")
202+
else:
203+
raise RuntimeError(f"failed to run: {message}") from err
204+
205+
206+
def prepare_new_release(level):
207+
"""Prepare files for a new release on GitHub."""
208+
try:
209+
last_version_number = get_last_version().strip("v")
210+
except subprocess.CalledProcessError as err:
211+
if "release not found" in err.stderr.decode("utf8"):
212+
# The project doesn't have any releases yet.
213+
last_version_number = "0.0.0"
214+
else:
215+
raise
216+
new_version_number = bump_version_number(last_version_number, level)
217+
218+
update_setup(new_version_number).gitadd()
219+
update_version(new_version_number).gitadd()
220+
update_changelog(new_version_number).gitadd()
221+
update_readme(new_version_number).gitadd()
222+
223+
Git().commit(new_version_number)
224+
225+
226+
if __name__ == "__main__":
227+
from sys import argv
228+
try:
229+
LEVEL = argv[1]
230+
except IndexError:
231+
LEVEL = "patch"
232+
prepare_new_release(LEVEL)

.github/scripts/setup_devbranch.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
#!/usr/bin/env python3
2+
"""This script is part of the GitHub CI postrelease-setup-devbranch pipeline
3+
4+
The following preparation steps are executed:
5+
6+
- update version numbers in _version.py and setup.py: append a -dev suffix
7+
- insert a vanilla "unreleased" section on top of CHANGELOG.md
8+
9+
The changes are not commited to the repository. This is dealt with in the bash script
10+
`setup_devbranch.sh` (which is also the caller of this script).
11+
"""
12+
import glob
13+
import json
14+
import re
15+
import subprocess
16+
17+
18+
def get_last_version() -> str:
19+
"""Return the version number of the last release."""
20+
json_string = (
21+
subprocess.run(
22+
["gh", "release", "view", "--json", "tagName"],
23+
check=True,
24+
stdout=subprocess.PIPE,
25+
stderr=subprocess.PIPE,
26+
)
27+
.stdout.decode("utf8")
28+
.strip()
29+
)
30+
31+
return json.loads(json_string)["tagName"]
32+
33+
34+
def update_changelog():
35+
"""Insert a vanilla "Unreleased" section on top."""
36+
with open("CHANGELOG.md", 'r', encoding="UTF-8") as changelog:
37+
lines = changelog.readlines()
38+
39+
if "## Unreleased" in lines:
40+
return
41+
42+
with open("CHANGELOG.md", 'w', encoding="UTF-8") as changelog:
43+
changelog.write("""# Changelog
44+
45+
## Unreleased
46+
47+
Release date: YYYY-MM-DD
48+
49+
Code freeze date: YYYY-MM-DD
50+
51+
### Description
52+
53+
### Dependency Changes
54+
55+
### Added
56+
57+
### Changed
58+
59+
### Fixed
60+
61+
### Deprecated
62+
63+
### Removed
64+
65+
""")
66+
changelog.writelines(lines[2:])
67+
68+
69+
def update_version(nvn):
70+
"""Update the _version.py file"""
71+
[file_with_version] = glob.glob("climada*/_version.py")
72+
regex = r'(^__version__\s*=\s*[\'\"]).*([\'\"]\s*$)'
73+
return update_file(file_with_version, regex, nvn)
74+
75+
76+
def update_setup(new_version_number):
77+
"""Update the setup.py file"""
78+
file_with_version = "setup.py"
79+
regex = r'(^\s+version\s*=\s*[\'\"]).*([\'\"]\s*,\s*$)'
80+
return update_file(file_with_version, regex, new_version_number)
81+
82+
83+
def update_file(file_with_version, regex, new_version_number):
84+
"""Replace the version number(s) in a file, based on a rgular expression."""
85+
with open(file_with_version, 'r', encoding="UTF-8") as curf:
86+
lines = curf.readlines()
87+
successfully_updated = False
88+
for i, line in enumerate(lines):
89+
mtch = re.match(regex, line)
90+
if mtch:
91+
lines[i] = f"{mtch.group(1)}{new_version_number}{mtch.group(2)}"
92+
successfully_updated = True
93+
if not successfully_updated:
94+
raise RuntimeError(f"cannot determine version of {file_with_version}")
95+
with open(file_with_version, 'w', encoding="UTF-8") as newf:
96+
for line in lines:
97+
newf.write(line)
98+
99+
100+
def setup_devbranch():
101+
"""Adjust files after a release was published, i.e.,
102+
apply the canonical deviations from main in develop.
103+
104+
Just changes files, all `git` commands are in the setup_devbranch.sh file.
105+
"""
106+
main_version = get_last_version().strip('v')
107+
108+
dev_version = f"{main_version}-dev"
109+
110+
update_setup(dev_version)
111+
update_version(dev_version)
112+
update_changelog()
113+
114+
print(f"v{dev_version}")
115+
116+
117+
if __name__ == "__main__":
118+
setup_devbranch()

0 commit comments

Comments
 (0)