Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions tools/github/release/BUILD
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
load("@github_pip3//:requirements.bzl", "requirement")
load("@rules_python//python:defs.bzl", "py_library")
load("//bazel:envoy_build_system.bzl", "envoy_package")
load("//tools/base:envoy_python.bzl", "envoy_py_library")

licenses(["notice"]) # Apache 2

Expand All @@ -22,3 +23,17 @@ py_library(
name = "exceptions",
srcs = ["exceptions.py"],
)

envoy_py_library(
"tools.github.release.manager",
deps = [
":abstract",
":exceptions",
"//tools/base:abstract",
"//tools/base:functional",
"//tools/base:utils",
requirement("aiohttp"),
requirement("gidgethub"),
requirement("packaging"),
],
)
145 changes: 145 additions & 0 deletions tools/github/release/manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import pathlib
import re
from functools import cached_property
from typing import Dict, List, Optional, Pattern, Type, Union

import verboselogs # type:ignore

import packaging.version

import aiohttp

import gidgethub.abc
import gidgethub.aiohttp

from tools.base import abstract
from tools.base.functional import async_property

from tools.github.release.abstract import AGithubRelease, AGithubReleaseManager
from tools.github.release.exceptions import GithubReleaseError
# from tools.github.release.release import GithubRelease

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in order to land them separately i commented out the interdependent bits in the tests and class


VERSION_MIN = packaging.version.Version("0")


@abstract.implementer(AGithubReleaseManager)
class GithubReleaseManager:

_version_re = r"v(\w+)"
_version_format = "v{version}"

def __init__(
self,
path: Union[str, pathlib.Path],
repository: str,
continues: Optional[bool] = False,
create: Optional[bool] = True,
user: Optional[str] = None,
oauth_token: Optional[str] = None,
version: Optional[str] = None,
log: Optional[verboselogs.VerboseLogger] = None,
asset_types: Optional[Dict[str, Pattern[str]]] = None,
github: Optional[gidgethub.abc.GitHubAPI] = None,
session: Optional[aiohttp.ClientSession] = None) -> None:
self.version = version
self._path = path
self.repository = repository
self.continues = continues
self._log = log
self.oauth_token = oauth_token
self.user = user
self._asset_types = asset_types
self._github = github
self._session = session
self.create = create

async def __aenter__(self) -> AGithubReleaseManager:
return self

async def __aexit__(self, *args) -> None:
await self.close()

def __getitem__(self, version) -> AGithubRelease:
# return self.release_class(self, version)
raise NotImplementedError

@property
def release_class(self) -> Type[AGithubRelease]:
# return GithubRelease
raise NotImplementedError

@cached_property
def github(self) -> gidgethub.abc.GitHubAPI:
return (
self._github
or gidgethub.aiohttp.GitHubAPI(self.session, self.user, oauth_token=self.oauth_token))

@cached_property
def log(self) -> verboselogs.VerboseLogger:
return self._log or verboselogs.VerboseLogger(__name__)

@cached_property
def path(self) -> pathlib.Path:
return pathlib.Path(self._path)

@async_property
async def latest(self) -> Dict[str, packaging.version.Version]:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this entire class and manager just a very elaborate way to get at latest version? The contrast is some of what we do in https://github.com/envoyproxy/envoy/blob/main/tools/dependency/release_dates.py. That's also not very concise.

I thought based on the comments in the abstract interface, this would actually be doing some artifact fetch/push.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class does quite a lot

Overall the tool is structured broadly into 3 classes

GithubRelease - CRUD operations
GithubReleaseAssets - upload/download of assets

and this one GithubReleaseManager - which:

  • provides logging and warn/error handling
  • holds the github connection and http client session
  • iterates releases and creates a dictionary of latest major/minor versions
  • version formatting/parsing (ie v0.0.1 -> packaging.Version("0.0.1"))
  • provides a factory for GithubRelease objects

its my intention to rewrite the release dates util at some point to make use of a Runner, improve logging/error handling, add tests etc. In that event the release dates util can almost certainly make use of this lib

I thought based on the comments in the abstract interface, this would actually be doing some artifact fetch/push.

if you look through the interfaces again, i think you will see that this is an implementation of the manager class specified there

artifact fetch/push comes after

latest = {}
for release in await self.releases:
version = self.parse_version(release["tag_name"])
if not version:
continue
latest[str(version)] = version
minor = f"{version.major}.{version.minor}"
if version > latest.get(minor, self.version_min):
latest[minor] = version
return latest

@async_property
async def releases(self) -> List[Dict]:
results = []
# By iterating the results from the releases url here,
# gidgethub will paginate the release information.
async for result in self.github.getiter(str(self.releases_url)):
Comment thread
phlax marked this conversation as resolved.
results.append(result)
return results

@cached_property
def releases_url(self) -> pathlib.PurePosixPath:
return pathlib.PurePosixPath(f"/repos/{self.repository}/releases")

@cached_property
def session(self) -> aiohttp.ClientSession:
return self._session or aiohttp.ClientSession()

@property
def version_min(self) -> packaging.version.Version:
return VERSION_MIN

@cached_property
def version_re(self) -> Pattern[str]:
return re.compile(self._version_re)

async def close(self) -> None:
if not "session" in self.__dict__:
return
await self.session.close()
del self.__dict__["session"]

def fail(self, message: str) -> str:
if not self.continues:
raise GithubReleaseError(message)
self.log.warning(message)
return message

def format_version(self, version: Union[str, packaging.version.Version]) -> str:
return self._version_format.format(version=version)

def parse_version(self, version: str) -> Optional[packaging.version.Version]:
parsed_version = self.version_re.sub(r"\1", version)
if parsed_version:
try:
return packaging.version.Version(parsed_version)
except packaging.version.InvalidVersion:
pass
self.log.warning(f"Unable to parse version: {version}")
Loading