release.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. # Copyright 2020 The Matrix.org Foundation C.I.C.
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License");
  6. # you may not use this file except in compliance with the License.
  7. # You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. """An interactive script for doing a release. See `run()` below.
  17. """
  18. import subprocess
  19. import sys
  20. from typing import Optional
  21. import click
  22. import git
  23. from packaging import version
  24. from redbaron import RedBaron
  25. @click.command()
  26. def run():
  27. """An interactive script to walk through the initial stages of creating a
  28. release, including creating release branch, updating changelog and pushing to
  29. GitHub.
  30. Requires the dev dependencies be installed, which can be done via:
  31. pip install -e .[dev]
  32. """
  33. # Make sure we're in a git repo.
  34. try:
  35. repo = git.Repo()
  36. except git.InvalidGitRepositoryError:
  37. raise click.ClickException("Not in Synapse repo.")
  38. if repo.is_dirty():
  39. raise click.ClickException("Uncommitted changes exist.")
  40. click.secho("Updating git repo...")
  41. repo.remote().fetch()
  42. # Parse the AST and load the `__version__` node so that we can edit it
  43. # later.
  44. with open("synapse/__init__.py") as f:
  45. red = RedBaron(f.read())
  46. version_node = None
  47. for node in red:
  48. if node.type != "assignment":
  49. continue
  50. if node.target.type != "name":
  51. continue
  52. if node.target.value != "__version__":
  53. continue
  54. version_node = node
  55. break
  56. if not version_node:
  57. print("Failed to find '__version__' definition in synapse/__init__.py")
  58. sys.exit(1)
  59. # Parse the current version.
  60. current_version = version.parse(version_node.value.value.strip('"'))
  61. assert isinstance(current_version, version.Version)
  62. # Figure out what sort of release we're doing and calcuate the new version.
  63. rc = click.confirm("RC", default=True)
  64. if current_version.pre:
  65. # If the current version is an RC we don't need to bump any of the
  66. # version numbers (other than the RC number).
  67. base_version = "{}.{}.{}".format(
  68. current_version.major,
  69. current_version.minor,
  70. current_version.micro,
  71. )
  72. if rc:
  73. new_version = "{}.{}.{}rc{}".format(
  74. current_version.major,
  75. current_version.minor,
  76. current_version.micro,
  77. current_version.pre[1] + 1,
  78. )
  79. else:
  80. new_version = base_version
  81. else:
  82. # If this is a new release cycle then we need to know if its a major
  83. # version bump or a hotfix.
  84. release_type = click.prompt(
  85. "Release type",
  86. type=click.Choice(("major", "hotfix")),
  87. show_choices=True,
  88. default="major",
  89. )
  90. if release_type == "major":
  91. base_version = new_version = "{}.{}.{}".format(
  92. current_version.major,
  93. current_version.minor + 1,
  94. 0,
  95. )
  96. if rc:
  97. new_version = "{}.{}.{}rc1".format(
  98. current_version.major,
  99. current_version.minor + 1,
  100. 0,
  101. )
  102. else:
  103. base_version = new_version = "{}.{}.{}".format(
  104. current_version.major,
  105. current_version.minor,
  106. current_version.micro + 1,
  107. )
  108. if rc:
  109. new_version = "{}.{}.{}rc1".format(
  110. current_version.major,
  111. current_version.minor,
  112. current_version.micro + 1,
  113. )
  114. # Confirm the calculated version is OK.
  115. if not click.confirm(f"Create new version: {new_version}?", default=True):
  116. click.get_current_context().abort()
  117. # Switch to the release branch.
  118. release_branch_name = f"release-v{base_version}"
  119. release_branch = find_ref(repo, release_branch_name)
  120. if release_branch:
  121. if release_branch.is_remote():
  122. # If the release branch only exists on the remote we check it out
  123. # locally.
  124. repo.git.checkout(release_branch_name)
  125. release_branch = repo.active_branch
  126. else:
  127. # If a branch doesn't exist we create one. We ask which one branch it
  128. # should be based off, defaulting to sensible values depending on the
  129. # release type.
  130. if current_version.is_prerelease:
  131. default = release_branch_name
  132. elif release_type == "major":
  133. default = "develop"
  134. else:
  135. default = "master"
  136. branch_name = click.prompt(
  137. "Which branch should the release be based on?", default=default
  138. )
  139. base_branch = find_ref(repo, branch_name)
  140. if not base_branch:
  141. print(f"Could not find base branch {branch_name}!")
  142. click.get_current_context().abort()
  143. # Check out the base branch and ensure it's up to date
  144. repo.head.reference = base_branch
  145. repo.head.reset(index=True, working_tree=True)
  146. if not base_branch.is_remote():
  147. update_branch(repo)
  148. # Create the new release branch
  149. release_branch = repo.create_head(release_branch_name, commit=base_branch)
  150. # Switch to the release branch and ensure its up to date.
  151. repo.git.checkout(release_branch_name)
  152. update_branch(repo)
  153. # Update the `__version__` variable and write it back to the file.
  154. version_node.value = '"' + new_version + '"'
  155. with open("synapse/__init__.py", "w") as f:
  156. f.write(red.dumps())
  157. # Generate changelogs
  158. subprocess.run("python3 -m towncrier", shell=True)
  159. # Generate debian changelogs if its not an RC.
  160. if not rc:
  161. subprocess.run(
  162. f'dch -M -v {new_version} "New synapse release {new_version}."', shell=True
  163. )
  164. subprocess.run('dch -M -r -D stable ""', shell=True)
  165. # Show the user the changes and ask if they want to edit the change log.
  166. repo.git.add("-u")
  167. subprocess.run("git diff --cached", shell=True)
  168. if click.confirm("Edit changelog?", default=False):
  169. click.edit(filename="CHANGES.md")
  170. # Commit the changes.
  171. repo.git.add("-u")
  172. repo.git.commit(f"-m {new_version}")
  173. # We give the option to bail here in case the user wants to make sure things
  174. # are OK before pushing.
  175. if not click.confirm("Push branch to github?", default=True):
  176. print("")
  177. print("Run when ready to push:")
  178. print("")
  179. print(f"\tgit push -u {repo.remote().name} {repo.active_branch.name}")
  180. print("")
  181. sys.exit(0)
  182. # Otherwise, push and open the changelog in the browser.
  183. repo.git.push("-u", repo.remote().name, repo.active_branch.name)
  184. click.launch(
  185. f"https://github.com/matrix-org/synapse/blob/{repo.active_branch.name}/CHANGES.md"
  186. )
  187. def find_ref(repo: git.Repo, ref_name: str) -> Optional[git.HEAD]:
  188. """Find the branch/ref, looking first locally then in the remote."""
  189. if ref_name in repo.refs:
  190. return repo.refs[ref_name]
  191. elif ref_name in repo.remote().refs:
  192. return repo.remote().refs[ref_name]
  193. else:
  194. return None
  195. def update_branch(repo: git.Repo):
  196. """Ensure branch is up to date if it has a remote"""
  197. if repo.active_branch.tracking_branch():
  198. repo.git.merge(repo.active_branch.tracking_branch().name)
  199. if __name__ == "__main__":
  200. run()