text: #!/usr/bin/env python
  import json
  import os
  import re
  import subprocess
  from typing import Optional
  
  import packaging.version
  
  
  DEV_BRANCH_PATTERN = re.compile(r"^(.*/)?([0-9]+)\.x$")
  RELEASE_BRANCH_PATTERN = re.compile(r"^(.*/)?([0-9]+\.[0-9]+)$")
  
  
  def _mock_git_remote_tags(versions):
      # type: (list[str]) -> None
      """Helper used in tests to mock the output from ``git ls-remote --tags origin``"""
      import binascii
      import random
      from unittest import mock
  
      output = b"\n".join(
          [
              b"\t".join([binascii.hexlify(random.randbytes(20)), bytes(f"refs/tags/{version}", "utf-8")])
              for version in versions
          ]
      )
  
      mock.patch("subprocess.check_output", return_value=output).start()
  
  
  def get_base_branch():
      # type: () -> str
      if "GITHUB_EVENT_PATH" not in os.environ:
          return os.environ.get("BASE_BRANCH", "origin/1.x")
  
      event = {}
      with open(os.environ["GITHUB_EVENT_PATH"], "r") as fp:
          event = json.load(fp)
  
      assert event, "Could not parse GitHub event from {}".format(os.environ["GITHUB_EVENT_PATH"])
  
      base_ref = event["pull_request"]["base"]["ref"]
      return base_ref
  
  
  def is_dev_branch(branch):
      # type: (str) -> bool
      """Determine if the provided branch name matches our dev branch pattern.
  
      # Normal expected dev branch names
      >>> is_dev_branch("0.x")
      True
      >>> is_dev_branch("0.x")
      True
      >>> is_dev_branch("1.x")
      True
      >>> is_dev_branch("27.x")
      True
      >>> is_dev_branch("origin/1.x")
      True
      >>> is_dev_branch("refs/remotes/origin/1.x")
      True
  
      # User feature branches do not match
      >>> is_dev_branch("feature-branch")
      False
      >>> is_dev_branch("user/feature-branch")
      False
  
      # Release branches do not match
      >>> is_dev_branch("0.59")
      False
      >>> is_dev_branch("1.0")
      False
      >>> is_dev_branch("27.5")
      False
      >>> is_dev_branch("origin/1.0")
      False
      """
      return DEV_BRANCH_PATTERN.match(branch) is not None
  
  
  def is_release_branch(branch):
      # type: (str) -> bool
      """Determine if the provided branch name matches our release branch pattern.
  
      # Release branches do not match
      >>> is_release_branch("0.59")
      True
      >>> is_release_branch("1.0")
      True
      >>> is_release_branch("27.5")
      True
      >>> is_release_branch("origin/1.0")
      True
  
      # Dev branches do not match
      >>> is_release_branch("0.x")
      False
      >>> is_release_branch("0.x")
      False
      >>> is_release_branch("1.x")
      False
      >>> is_release_branch("27.x")
      False
      >>> is_release_branch("origin/1.x")
      False
  
      # User feature branches do not match
      >>> is_release_branch("feature-branch")
      False
      >>> is_release_branch("user/feature-branch")
      False
      """
      return RELEASE_BRANCH_PATTERN.match(branch) is not None
  
  
  def get_next_minor_version(branch):
      # type: (str) -> str
      """For a given dev branch get the next minor version
  
      # Random order to validate sorting is used
      >>> _mock_git_remote_tags([
      ...   "v0.50.5", "v0.50.4", "v0.50.6", "v0.51.0rc1", "v0.49.5", "v1.4.6",
      ...   "v1.20.5", "v0.34.0", "v6.0.0", "v5.0.0rc5"
      ... ])
      >>> get_next_minor_version("1.x")
      'v1.21.0'
      >>> get_next_minor_version("2.x")  # doesn't exist
      'v2.0.0'
      >>> get_next_minor_version("0.x")
      'v0.52.0'
      >>> get_next_minor_version("6.x")
      'v6.1.0'
      >>> get_next_minor_version("5.x")
      'v5.1.0'
      """
      match = DEV_BRANCH_PATTERN.match(branch)
      if not match:
          raise Exception("Cannot get next minor from branch {!r}".format(branch))
  
      release_line = int(match.group(2))
      release_versions = get_versions(major=release_line)
      if release_versions:
          most_recent = release_versions[0]
          return "v{}.{}.0".format(most_recent.major, most_recent.minor + 1)
      return "v{}.0.0".format(release_line)
  
  
  def get_next_patch_version(branch):
      # type: (str) -> str
      """For a given release branch get the next patch version
  
      # Random order to validate sorting is used
      >>> _mock_git_remote_tags([
      ...   "v0.50.5", "v0.50.4", "v0.50.6", "v0.51.0rc1", "v0.49.5", "v1.4.6",
      ...   "v1.20.5", "v0.34.0", "v6.0.0", "v5.0.0rc5"
      ... ])
      >>> get_next_patch_version("1.4")
      'v1.4.7'
      >>> get_next_patch_version("2.0")  # doesn't exist
      'v2.0.0'
      >>> get_next_patch_version("0.51")  # don't increment if most recent is an rc
      'v0.51.0'
      >>> get_next_patch_version("6.0")
      'v6.0.1'
      >>> get_next_patch_version("1.20")
      'v1.20.6'
      """
      match = RELEASE_BRANCH_PATTERN.match(branch)
      if not match:
          raise Exception("Cannot get next patch from branch {!r}".format(branch))
  
      release = packaging.version.Version(match.group(2))
      release_versions = get_versions(major=release.major, minor=release.minor)
      if not release_versions:
          return "v{}.{}.0".format(release.major, release.minor)
  
      most_recent = release_versions[0]
  
      # If the most recent is a dev/prerelease then use that micro version, otherwise latest + 1
      if most_recent.is_devrelease or most_recent.is_prerelease:
          return "v{}.{}.{}".format(most_recent.major, most_recent.minor, most_recent.micro)
      return "v{}.{}.{}".format(most_recent.major, most_recent.minor, most_recent.micro + 1)
  
  
  def get_versions(major, minor=None):
      # type: (int, Optional[int]) -> list[packaging.version.Version]
      """Get the currently released versions of this package from git remote tags
  
      >>> from packaging.version import Version
      >>> from unittest import mock
      >>> output = b'''
      ... 84e27ad91a10e57fd933e2e14c9216eab92d67c3	refs/tags/docs
      ... 84e27ad91a10e57fd933e2e14c9216eab92d67c4	refs/tags/non-release-tag
      ... 3956b6f2cd44dfd07133a8b28c91f1400b14aa7a	refs/tags/v0.49.0
      ... 0562918f51bde8707899759ee240669d6ff7be38	refs/tags/v0.49.0rc1
      ... 39f5079d5bfa34918d3d09c814c98df8832986c6	refs/tags/v0.49.1
      ... 9ee8ef7d2fef9fd735695b180caed42c317559cf	refs/tags/v0.50.0
      ... dba326054b6ecc7e6e6a0e0dfe7bf7d2caff3342	refs/tags/v0.50.0rc1
      ... 465edcd4e934e2f2090f7e3ff43cd3b644a43c01	refs/tags/v0.50.1
      ... 1a6bad3ec2cbfffab0f83ebf5b72fe786b76dbed	refs/tags/v0.59.0
      ... 7c18eca77ddda6b758d91c26c62714e0aabb9592	refs/tags/v0.59.0rc1
      ... 19d4c27089e32e6419e8261a7fa96779ea9699ed	refs/tags/v1.0.0rc1
      ... '''
      >>> mock.patch("subprocess.check_output", return_value=output).start()  # doctest: +ELLIPSIS
      <MagicMock name='check_output' id=...>
  
      # Only getting the major version
      >>> get_versions(major=1)
      [<Version('1.0.0rc1')>]
      >>> get_versions(major=0)
      [<Version('0.59.0')>, <Version('0.59.0rc1')>, <Version('0.50.1')>, <Version('0.50.0')>, <Version('0.50.0rc1')>, <Version('0.49.1')>, <Version('0.49.0')>, <Version('0.49.0rc1')>]
      >>> get_versions(major=45)
      []
  
      # Checking major and minor version
      >>> get_versions(major=0, minor=50)
      [<Version('0.50.1')>, <Version('0.50.0')>, <Version('0.50.0rc1')>]
      >>> get_versions(major=1, minor=0)
      [<Version('1.0.0rc1')>]
      >>> get_versions(major=1, minor=2)
      []
  
      # No results from git
      >>> mock.patch("subprocess.check_output", return_value=b"").start()  # doctest: +ELLIPSIS
      <MagicMock name='check_output' id=...>
      >>> get_versions(major=0)
      []
      """  # noqa: E501
      output = subprocess.check_output(["git", "ls-remote", "--tags", "origin"])
  
      versions = []
      for raw_line in output.splitlines():
          line = raw_line.decode().strip()
          if not line:
              continue
  
          _, _, tag = line.partition("refs/tags/")
          try:
              version = packaging.version.Version(tag)
          except Exception:
              continue
  
          # Filter
          if version.major != major:
              continue
          elif minor is not None and version.minor != minor:
              continue
  
          versions.append(version)
  
      # Sort most recent first
      return sorted(versions, reverse=True)
  
  
  def main():
      # type: () -> None
      base_branch = get_base_branch()
  
      if is_dev_branch(base_branch):
          print("::set-output name=milestone::{}".format(get_next_minor_version(base_branch)))
      elif is_release_branch(base_branch):
          print("::set-output name=milestone::{}".format(get_next_patch_version(base_branch)))
  
  
  if __name__ == "__main__":
      main()
  