|
2 | 2 |
|
3 | 3 | """A utility script for automating the beets release process.
|
4 | 4 | """
|
| 5 | +import argparse |
5 | 6 | import datetime
|
6 | 7 | import os
|
7 | 8 | import re
|
8 |
| -import subprocess |
9 |
| -from contextlib import contextmanager |
10 |
| - |
11 |
| -import click |
12 | 9 |
|
13 | 10 | BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
14 | 11 | CHANGELOG = os.path.join(BASE, "docs", "changelog.rst")
|
15 |
| - |
16 |
| - |
17 |
| -@contextmanager |
18 |
| -def chdir(d): |
19 |
| - """A context manager that temporary changes the working directory.""" |
20 |
| - olddir = os.getcwd() |
21 |
| - os.chdir(d) |
22 |
| - yield |
23 |
| - os.chdir(olddir) |
24 |
| - |
25 |
| - |
26 |
| -@click.group() |
27 |
| -def release(): |
28 |
| - pass |
29 |
| - |
| 12 | +parser = argparse.ArgumentParser() |
| 13 | +parser.add_argument("version", type=str) |
30 | 14 |
|
31 | 15 | # Locations (filenames and patterns) of the version number.
|
32 | 16 | VERSION_LOCS = [
|
@@ -67,7 +51,7 @@ def release():
|
67 | 51 | GITHUB_REPO = "beets"
|
68 | 52 |
|
69 | 53 |
|
70 |
| -def bump_version(version): |
| 54 | +def bump_version(version: str): |
71 | 55 | """Update the version number in setup.py, docs config, changelog,
|
72 | 56 | and root module.
|
73 | 57 | """
|
@@ -105,129 +89,45 @@ def bump_version(version):
|
105 | 89 |
|
106 | 90 | found = True
|
107 | 91 | break
|
108 |
| - |
109 | 92 | else:
|
110 | 93 | # Normal line.
|
111 | 94 | out_lines.append(line)
|
112 |
| - |
113 | 95 | if not found:
|
114 | 96 | print(f"No pattern found in {filename}")
|
115 |
| - |
116 | 97 | # Write the file back.
|
117 | 98 | with open(filename, "w") as f:
|
118 | 99 | f.write("".join(out_lines))
|
119 | 100 |
|
120 |
| - # Insert into the right place. |
121 |
| - with open(CHANGELOG) as f: |
122 |
| - contents = f.read() |
123 |
| - location = contents.find("\n\n") # First blank line. |
124 |
| - contents = contents[:location] + contents[location:] |
125 |
| - |
126 |
| - # Write back. |
127 |
| - with open(CHANGELOG, "w") as f: |
128 |
| - f.write(contents) |
129 |
| - |
130 |
| - |
131 |
| -@release.command() |
132 |
| -@click.argument("version") |
133 |
| -def bump(version): |
134 |
| - """Bump the version number.""" |
135 |
| - bump_version(version) |
| 101 | + update_changelog(version) |
136 | 102 |
|
137 | 103 |
|
138 |
| -def get_latest_changelog(): |
139 |
| - """Extract the first section of the changelog.""" |
140 |
| - started = False |
141 |
| - lines = [] |
142 |
| - with open(CHANGELOG) as f: |
143 |
| - for line in f: |
144 |
| - if re.match(r"^--+$", line.strip()): |
145 |
| - # Section boundary. Start or end. |
146 |
| - if started: |
147 |
| - # Remove last line, which is the header of the next |
148 |
| - # section. |
149 |
| - del lines[-1] |
150 |
| - break |
151 |
| - else: |
152 |
| - started = True |
153 |
| - |
154 |
| - elif started: |
155 |
| - lines.append(line) |
156 |
| - return "".join(lines[2:]).strip() |
157 |
| - |
158 |
| - |
159 |
| -def rst2md(text): |
160 |
| - """Use Pandoc to convert text from ReST to Markdown.""" |
161 |
| - pandoc = subprocess.Popen( |
162 |
| - ["pandoc", "--from=rst", "--to=markdown", "--wrap=none"], |
163 |
| - stdin=subprocess.PIPE, |
164 |
| - stdout=subprocess.PIPE, |
165 |
| - stderr=subprocess.PIPE, |
| 104 | +def update_changelog(version: str): |
| 105 | + # Generate bits to insert into changelog. |
| 106 | + header_line = f"{version} (in development)" |
| 107 | + header = "\n\n" + header_line + "\n" + "-" * len(header_line) + "\n\n" |
| 108 | + header += ( |
| 109 | + "Changelog goes here! Please add your entry to the bottom of" |
| 110 | + " one of the lists below!\n" |
166 | 111 | )
|
167 |
| - stdout, _ = pandoc.communicate(text.encode("utf-8")) |
168 |
| - md = stdout.decode("utf-8").strip() |
169 |
| - |
170 |
| - # Fix up odd spacing in lists. |
171 |
| - return re.sub(r"^- ", "- ", md, flags=re.M) |
172 |
| - |
173 |
| - |
174 |
| -def changelog_as_markdown(): |
175 |
| - """Get the latest changelog entry as hacked up Markdown.""" |
176 |
| - rst = get_latest_changelog() |
177 |
| - |
178 |
| - # Replace plugin links with plugin names. |
179 |
| - rst = re.sub(r":doc:`/plugins/(\w+)`", r"``\1``", rst) |
180 |
| - |
181 |
| - # References with text. |
182 |
| - rst = re.sub(r":ref:`([^<]+)(<[^>]+>)`", r"\1", rst) |
183 |
| - |
184 |
| - # Other backslashes with verbatim ranges. |
185 |
| - rst = re.sub(r"(\s)`([^`]+)`([^_])", r"\1``\2``\3", rst) |
186 |
| - |
187 |
| - # Command links with command names. |
188 |
| - rst = re.sub(r":ref:`(\w+)-cmd`", r"``\1``", rst) |
189 |
| - |
190 |
| - # Bug numbers. |
191 |
| - rst = re.sub(r":bug:`(\d+)`", r"#\1", rst) |
192 |
| - |
193 |
| - # Users. |
194 |
| - rst = re.sub(r":user:`(\w+)`", r"@\1", rst) |
195 |
| - |
196 |
| - # Convert with Pandoc. |
197 |
| - md = rst2md(rst) |
198 |
| - |
199 |
| - # Restore escaped issue numbers. |
200 |
| - md = re.sub(r"\\#(\d+)\b", r"#\1", md) |
201 |
| - |
202 |
| - return md |
203 |
| - |
204 |
| - |
205 |
| -@release.command() |
206 |
| -def changelog(): |
207 |
| - """Get the most recent version's changelog as Markdown.""" |
208 |
| - print(changelog_as_markdown()) |
209 |
| - |
210 |
| - |
211 |
| -def get_version(index=0): |
212 |
| - """Read the current version from the changelog.""" |
| 112 | + # Insert into the right place. |
213 | 113 | with open(CHANGELOG) as f:
|
214 |
| - cur_index = 0 |
215 |
| - for line in f: |
216 |
| - match = re.search(r"^\d+\.\d+\.\d+", line) |
217 |
| - if match: |
218 |
| - if cur_index == index: |
219 |
| - return match.group(0) |
220 |
| - else: |
221 |
| - cur_index += 1 |
| 114 | + contents = f.readlines() |
222 | 115 |
|
| 116 | + contents = [ |
| 117 | + line |
| 118 | + for line in contents |
| 119 | + if not re.match(r"Changelog goes here!.*", line) |
| 120 | + ] |
| 121 | + contents = "".join(contents) |
| 122 | + contents = re.sub("\n{3,}", "\n\n", contents) |
223 | 123 |
|
224 |
| -@release.command() |
225 |
| -def version(): |
226 |
| - """Display the current version.""" |
227 |
| - print(get_version()) |
| 124 | + location = contents.find("\n\n") # First blank line. |
| 125 | + contents = contents[:location] + header + contents[location:] |
| 126 | + # Write back. |
| 127 | + with open(CHANGELOG, "w") as f: |
| 128 | + f.write(contents) |
228 | 129 |
|
229 | 130 |
|
230 |
| -@release.command() |
231 | 131 | def datestamp():
|
232 | 132 | """Enter today's date as the release date in the changelog."""
|
233 | 133 | dt = datetime.datetime.now()
|
@@ -255,108 +155,12 @@ def datestamp():
|
255 | 155 | f.write(line)
|
256 | 156 |
|
257 | 157 |
|
258 |
| -@release.command() |
259 |
| -def prep(): |
260 |
| - """Run all steps to prepare a release. |
261 |
| -
|
262 |
| - - Tag the commit. |
263 |
| - - Build the sdist package. |
264 |
| - - Generate the Markdown changelog to ``changelog.md``. |
265 |
| - - Bump the version number to the next version. |
266 |
| - """ |
267 |
| - cur_version = get_version() |
268 |
| - |
269 |
| - # Tag. |
270 |
| - subprocess.check_call(["git", "tag", f"v{cur_version}"]) |
271 |
| - |
272 |
| - # Build. |
273 |
| - with chdir(BASE): |
274 |
| - subprocess.check_call(["python", "setup.py", "sdist"]) |
275 |
| - |
276 |
| - # Generate Markdown changelog. |
277 |
| - cl = changelog_as_markdown() |
278 |
| - with open(os.path.join(BASE, "changelog.md"), "w") as f: |
279 |
| - f.write(cl) |
280 |
| - |
| 158 | +def prep(args: argparse.Namespace): |
281 | 159 | # Version number bump.
|
282 |
| - # FIXME It should be possible to specify this as an argument. |
283 |
| - version_parts = [int(n) for n in cur_version.split(".")] |
284 |
| - version_parts[-1] += 1 |
285 |
| - next_version = ".".join(map(str, version_parts)) |
286 |
| - bump_version(next_version) |
287 |
| - |
288 |
| - |
289 |
| -@release.command() |
290 |
| -def publish(): |
291 |
| - """Unleash a release unto the world. |
292 |
| -
|
293 |
| - - Push the tag to GitHub. |
294 |
| - - Upload to PyPI. |
295 |
| - """ |
296 |
| - version = get_version(1) |
297 |
| - |
298 |
| - # Push to GitHub. |
299 |
| - with chdir(BASE): |
300 |
| - subprocess.check_call(["git", "push"]) |
301 |
| - subprocess.check_call(["git", "push", "--tags"]) |
302 |
| - |
303 |
| - # Upload to PyPI. |
304 |
| - path = os.path.join(BASE, "dist", f"beets-{version}.tar.gz") |
305 |
| - subprocess.check_call(["twine", "upload", path]) |
306 |
| - |
307 |
| - |
308 |
| -@release.command() |
309 |
| -def ghrelease(): |
310 |
| - """Create a GitHub release using the `github-release` command-line |
311 |
| - tool. |
312 |
| -
|
313 |
| - Reads the changelog to upload from `changelog.md`. Uploads the |
314 |
| - tarball from the `dist` directory. |
315 |
| - """ |
316 |
| - version = get_version(1) |
317 |
| - tag = "v" + version |
318 |
| - |
319 |
| - # Load the changelog. |
320 |
| - with open(os.path.join(BASE, "changelog.md")) as f: |
321 |
| - cl_md = f.read() |
322 |
| - |
323 |
| - # Create the release. |
324 |
| - subprocess.check_call( |
325 |
| - [ |
326 |
| - "github-release", |
327 |
| - "release", |
328 |
| - "-u", |
329 |
| - GITHUB_USER, |
330 |
| - "-r", |
331 |
| - GITHUB_REPO, |
332 |
| - "--tag", |
333 |
| - tag, |
334 |
| - "--name", |
335 |
| - f"{GITHUB_REPO} {version}", |
336 |
| - "--description", |
337 |
| - cl_md, |
338 |
| - ] |
339 |
| - ) |
340 |
| - |
341 |
| - # Attach the release tarball. |
342 |
| - tarball = os.path.join(BASE, "dist", f"beets-{version}.tar.gz") |
343 |
| - subprocess.check_call( |
344 |
| - [ |
345 |
| - "github-release", |
346 |
| - "upload", |
347 |
| - "-u", |
348 |
| - GITHUB_USER, |
349 |
| - "-r", |
350 |
| - GITHUB_REPO, |
351 |
| - "--tag", |
352 |
| - tag, |
353 |
| - "--name", |
354 |
| - os.path.basename(tarball), |
355 |
| - "--file", |
356 |
| - tarball, |
357 |
| - ] |
358 |
| - ) |
| 160 | + datestamp() |
| 161 | + bump_version(args.version) |
359 | 162 |
|
360 | 163 |
|
361 | 164 | if __name__ == "__main__":
|
362 |
| - release() |
| 165 | + args = parser.parse_args() |
| 166 | + prep(args) |
0 commit comments