Skip to content

Commit 5b42535

Browse files
author
Matt Woodward
committed
Better script
1 parent 10b768f commit 5b42535

File tree

1 file changed

+361
-0
lines changed

1 file changed

+361
-0
lines changed

scripts/tag.py

+361
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,361 @@
1+
#!/usr/bin/env python
2+
3+
import sys
4+
from datetime import datetime
5+
import os
6+
import re
7+
import argparse
8+
import subprocess
9+
10+
DOCKER_IMAGE = "swiftnav/libsbp-build:2023-12-19"
11+
PWD = os.getcwd()
12+
13+
PREP_FOR_NEXT_RELEASE_FINISHED_MSG = """
14+
A new commit has been generated and all language bindings rebuilt to take on a 'dirty' version number.
15+
16+
After dismissing this message the commit will be shown .If everything appears ok you can push to master straight away.
17+
18+
If there are any mistakes now is the time to correct them. Make any changes which are quired then update the tag by running:
19+
20+
git add <files>
21+
git commit --amend -m "prep for next release #no_auto_pr"
22+
23+
Once you have fixed everything you can push to master
24+
"""
25+
26+
TAG_FINISHED_MSG = """
27+
A new commit and tag have been created, all language bindings and documentation have been rebuilt, and the changelog updated.
28+
29+
After dismissing this message the commit will be shown. If everything looks good you can push to master straight away and continue with distribution.
30+
31+
If there are any mistakes now is the time to correct them. Make any changes which are required then update the tag by running:
32+
33+
git add <files>
34+
git commit --amend -a -m "Release <VERSION>"
35+
git tag -f -a <VERSION> -m "Version <VERSION> of libsbp."
36+
37+
Once you have fixed everything you can push to master
38+
39+
Once pushed prepare for the next release by running this script again with the "-p" flag.
40+
"""
41+
42+
COMMIT_MSG = ""
43+
TAG_MSG = ""
44+
TAG_NAME = ""
45+
46+
47+
def run_command(cmd: list, expect_success=True, docker=False):
48+
if docker:
49+
cmd = [
50+
"docker",
51+
"run",
52+
"-it",
53+
"--rm",
54+
"-v",
55+
f"{PWD}:/mnt/workspace",
56+
"-t",
57+
DOCKER_IMAGE,
58+
] + cmd
59+
60+
result = subprocess.run(cmd, capture_output=True, text=True)
61+
62+
if result.returncode != 0 and expect_success:
63+
print(f"Command failed: {cmd}")
64+
print(result.stdout)
65+
print(result.stderr)
66+
sys.exit(1)
67+
68+
return result.stdout
69+
70+
71+
def get_current_tag():
72+
return run_command(
73+
["git", "describe", "--match", "v*", "--always", "--tags"]
74+
).strip()
75+
76+
77+
def current_commit_is_tag():
78+
return re.match(r"^v[0-9]+\.[0-9]+\.[0-9]+$", get_current_tag())
79+
80+
81+
def get_next_tag():
82+
current_tag = get_current_tag()
83+
major, minor, patch = current_tag.split(".")[:3]
84+
85+
if "-" in patch:
86+
patch = patch.split("-")[0]
87+
88+
return f"{major}.{minor}.{int(patch)+1}"
89+
90+
91+
def fn_not_provided():
92+
assert False
93+
94+
95+
class Step:
96+
def __init__(self, name, fn=fn_not_provided, args={}):
97+
self.name = name
98+
self.fn = fn
99+
self.args = args
100+
101+
def run(self, index, total):
102+
print(f"[{index}/{total}] {self.name}")
103+
self.invoke()
104+
105+
def invoke(self):
106+
self.fn(**self.args)
107+
108+
109+
class CreateInitialCommit(Step):
110+
def __init__(self):
111+
Step.__init__(
112+
self,
113+
"Create initial commit",
114+
run_command,
115+
{"cmd": ["git", "commit", "--allow-empty", "-m", COMMIT_MSG]},
116+
)
117+
118+
119+
class CreateInitialTag(Step):
120+
def __init__(self):
121+
Step.__init__(
122+
self,
123+
"Create initial tag",
124+
run_command,
125+
{"cmd": ["git", "tag", "-a", args.tag, "-m", TAG_MSG]},
126+
)
127+
128+
129+
class UpdateCommit(Step):
130+
def __init__(self):
131+
Step.__init__(
132+
self,
133+
"Amend commit",
134+
run_command,
135+
{"cmd": ["git", "commit", "--amend", "-a", "-m", COMMIT_MSG]},
136+
)
137+
138+
139+
class UpdateTag(Step):
140+
def __init__(self):
141+
Step.__init__(
142+
self,
143+
"Update tag",
144+
run_command,
145+
{"cmd": ["git", "tag", "-f", "-a", args.tag, "-m", TAG_MSG]},
146+
)
147+
148+
149+
class BuildLanguages(Step):
150+
def __init__(self, languages):
151+
if args.generate_only:
152+
targets = ["gen-{}".format(lang) for lang in languages]
153+
else:
154+
targets = languages
155+
Step.__init__(
156+
self,
157+
f"Build {"" if args.generate_only else "and test "}languages: {', '.join(languages)}",
158+
run_command,
159+
{"cmd": ["make", *targets], "docker": True},
160+
)
161+
162+
163+
class BuildDocumentation(Step):
164+
def __init__(self):
165+
Step.__init__(
166+
self,
167+
"Build documentation",
168+
run_command,
169+
{"cmd": ["make", "docs"], "docker": True},
170+
)
171+
172+
173+
class GenerateDraftChangelog(Step):
174+
def __init__(self):
175+
Step.__init__(
176+
self,
177+
"Generate draft changelog",
178+
run_command,
179+
{"cmd": ["make", "release"]},
180+
)
181+
182+
183+
class MergeChangelogs(Step):
184+
def __init__(self):
185+
Step.__init__(self, "Merge changelogs")
186+
187+
def invoke(self):
188+
with open("DRAFT_CHANGELOG.md", "r") as f:
189+
draft = f.readlines()
190+
191+
# The first 4 lines are just the title and "unreleased" headers which we will recreate later
192+
draft = draft[4:]
193+
194+
# The first line should now be the first real line of the "unreleased" section which is always a link to the full changelog. Find the next heading and discard everything afterwards
195+
assert draft[0].startswith("[Full Changelog]")
196+
197+
for i in range(1, len(draft)):
198+
if draft[i].startswith("## [v"):
199+
draft = draft[: i - 1]
200+
break
201+
202+
proposed = [
203+
f"## [{args.tag}](https://github.com/swift-nav/libsbp/tree/{args.tag}) ({datetime.today().strftime('%Y-%m-%d')})\n",
204+
"\n",
205+
]
206+
207+
# Strip out anything which looks like a Jira ticket number
208+
for i in range(len(draft)):
209+
proposed.append(re.sub(r"\\\[[A-Z]*-[0-9]*\\\](?=[^(])", r"", draft[i]))
210+
proposed.append("\n")
211+
print("Proposed new changelog section")
212+
print("\n".join(proposed))
213+
214+
with open("CHANGELOG.md", "r") as f:
215+
changelog = f.readlines()
216+
217+
with open("CHANGELOG.md", "w") as f:
218+
# Keep the first 2 lines from the origin alchangelog
219+
f.writelines(changelog[0:2])
220+
221+
# Then the new section
222+
f.writelines(proposed)
223+
224+
# Then the rest of the original
225+
f.writelines(changelog[2:])
226+
227+
os.remove("DRAFT_CHANGELOG.md")
228+
229+
230+
class ShowFinishedBanner(Step):
231+
def __init__(self, msg):
232+
Step.__init__(self, "Finished")
233+
self.__msg = msg
234+
235+
def invoke(self):
236+
print(self.__msg)
237+
input("Press Enter to continue...")
238+
239+
240+
class ShowHead(Step):
241+
def __init__(self):
242+
Step.__init__(self, "Show head", run_command, {"cmd": ["git", "show", "HEAD"]})
243+
244+
245+
if __name__ == "__main__":
246+
if (
247+
not os.path.exists("spec")
248+
or not os.path.exists("generator")
249+
or not os.path.exists("scripts")
250+
):
251+
print("This script must be run from the root of the libsbp repository")
252+
sys.exit(1)
253+
254+
if (
255+
subprocess.run(["git", "diff", "--exit-code"], capture_output=True).returncode
256+
!= 0
257+
):
258+
print(
259+
"Working directory is not clean. Remove any and all changes before running this command"
260+
)
261+
sys.exit(1)
262+
263+
parser = argparse.ArgumentParser(
264+
description=f"When run without arguments will tag the next version of libsbp which will be {get_next_tag()}"
265+
)
266+
267+
parser.add_argument(
268+
"-t", "--tag", type=str, required=False, default=get_next_tag(), help="New tag"
269+
)
270+
parser.add_argument(
271+
"-p",
272+
"--prep_for_next_release",
273+
action="store_true",
274+
required=False,
275+
default=False,
276+
help="Prep for next release",
277+
)
278+
parser.add_argument(
279+
"-g",
280+
"--generate_only",
281+
action="store_true",
282+
required=False,
283+
default=False,
284+
help="Don't run tests, just generate sources",
285+
)
286+
287+
global args
288+
args = parser.parse_args()
289+
290+
steps = []
291+
292+
if args.prep_for_next_release:
293+
if not current_commit_is_tag():
294+
print("Can only prep for next release from a properly tagged commit")
295+
sys.exit(1)
296+
297+
COMMIT_MSG = "prep for next release #no_auto_pr"
298+
299+
steps.append(CreateInitialCommit())
300+
steps.append(BuildLanguages(["python"]))
301+
steps.append(UpdateCommit())
302+
steps.append(
303+
BuildLanguages(
304+
["java", "javascript", "protobuf", "c", "haskell", "javascript", "rust"]
305+
)
306+
)
307+
steps.append(UpdateCommit())
308+
steps.append(BuildLanguages(["javascript"]))
309+
steps.append(UpdateCommit())
310+
steps.append(BuildLanguages(["kaitai"]))
311+
steps.append(UpdateCommit())
312+
steps.append(ShowFinishedBanner(PREP_FOR_NEXT_RELEASE_FINISHED_MSG))
313+
steps.append(ShowHead())
314+
315+
else:
316+
COMMIT_MSG = f"Release {args.tag}"
317+
TAG_MSG = f"Version {args.tag} of libsbp."
318+
319+
if not re.match(r"^v[0-9]+\.[0-9]+\.[0-9]+$", args.tag):
320+
print(f"Invalid tag: {args.tag}")
321+
sys.exit(1)
322+
323+
if (
324+
subprocess.run(["git", "show", args.tag], capture_output=True).returncode
325+
== 0
326+
):
327+
print(f"Tag {args.tag} already exists")
328+
sys.exit(1)
329+
330+
input(f"About to release libsbp {args.tag}. Press Enter to continue...")
331+
332+
steps.append(CreateInitialCommit())
333+
steps.append(CreateInitialTag())
334+
steps.append(BuildLanguages(["python"]))
335+
steps.append(UpdateCommit())
336+
steps.append(UpdateTag())
337+
steps.append(
338+
BuildLanguages(
339+
["java", "javascript", "protobuf", "c", "haskell", "javascript", "rust"]
340+
)
341+
)
342+
steps.append(UpdateCommit())
343+
steps.append(UpdateTag())
344+
steps.append(BuildLanguages(["javascript"]))
345+
steps.append(UpdateCommit())
346+
steps.append(UpdateTag())
347+
steps.append(BuildLanguages(["kaitai"]))
348+
steps.append(UpdateCommit())
349+
steps.append(UpdateTag())
350+
steps.append(BuildDocumentation())
351+
steps.append(UpdateCommit())
352+
steps.append(UpdateTag())
353+
steps.append(GenerateDraftChangelog())
354+
steps.append(MergeChangelogs())
355+
steps.append(UpdateCommit())
356+
steps.append(UpdateTag())
357+
steps.append(ShowFinishedBanner(TAG_FINISHED_MSG))
358+
steps.append(ShowHead())
359+
360+
for i, step in enumerate(steps):
361+
step.run(i + 1, len(steps))

0 commit comments

Comments
 (0)