diff --git a/.github/actions/draft-release/update_draft_release.py b/.github/actions/draft-release/update_draft_release.py index a3ad580bc..6a2e91ccd 100755 --- a/.github/actions/draft-release/update_draft_release.py +++ b/.github/actions/draft-release/update_draft_release.py @@ -8,15 +8,14 @@ import github3 try: - import github.util + import github.release except ImportError: # make local development more comfortable repo_root = os.path.join(os.path.dirname(__file__), '../../..') sys.path.insert(1, repo_root) print(f'note: added {repo_root} to python-path (sys.path)') - import github.util + import github.release -import github.release import version as version_mod @@ -72,12 +71,7 @@ def main(): token=parsed.github_auth_token, ) - github_helper = github.util.GitHubRepositoryHelper( - owner=org, - name=repo, - github_api=github_api, - ) - repository = github_helper.repository + repository = github_api.repository(org, repo) with open(parsed.release_notes) as f: release_notes_md = f.read() @@ -101,7 +95,7 @@ def main(): print(f'Updating {draft_release_name=}') draft_release.edit(body=release_notes_md) - for release, deleted in github_helper.delete_outdated_draft_releases(): + for release, deleted in github.release.delete_outdated_draft_releases(repository): if deleted: print('Deleted obsolete draft {release.name=}') else: diff --git a/cli/gardener_ci/githubutil.py b/cli/gardener_ci/githubutil.py index 401ae1a09..9eaae0961 100644 --- a/cli/gardener_ci/githubutil.py +++ b/cli/gardener_ci/githubutil.py @@ -2,21 +2,12 @@ # # SPDX-License-Identifier: Apache-2.0 -import urllib - from ci.util import ( ctx, ) -from github.util import ( - GitHubRepositoryHelper, - find_greatest_github_release_version, - outdated_draft_releases, - -) +import github.release import ccc.github -import github3 - def list_draft_releases( github_cfg_name: str, @@ -38,23 +29,27 @@ def list_draft_releases( not equal to 0. ''' github_cfg = ctx().cfg_factory().github(github_cfg_name) - github_helper = GitHubRepositoryHelper( + github_api = ccc.github.github_api(github_cfg) + + repository = github_api.repository( owner=github_repository_owner, - name=github_repository_name, - github_api=ccc.github.github_api(github_cfg), + repository=github_repository_name, ) + if only_outdated: - releases = [release for release in github_helper.repository.releases()] + releases = [release for release in repository.releases()] non_draft_releases = [release for release in releases if not release.draft] - greatest_release_version = find_greatest_github_release_version(non_draft_releases) + greatest_release_version = github.release.find_greatest_github_release_version( + non_draft_releases, + ) else: - releases = github_helper.repository.releases() + releases = repository.releases() draft_releases = [release for release in releases if release.draft] if only_outdated: if greatest_release_version is not None: - draft_releases = outdated_draft_releases( + draft_releases = github.release.outdated_draft_releases( draft_releases=draft_releases, greatest_release_version=greatest_release_version, ) @@ -62,48 +57,3 @@ def list_draft_releases( draft_releases = [] for draft_release in draft_releases: print(draft_release.name) - - -def greatest_release_version( - github_repository_url: str, - anonymous: bool=False, - ignore_prereleases: bool=False, -): - '''Find the release with the greatest name (according to semver) and print its semver-version. - - Note: - - This will only consider releases whose names are either immediately parseable as semver- - versions, or prefixed with a single character ('v'). - - The 'v'-prefix (if present) will be not be present in the output. - - If a release has no name, its tag will be used instead of its name. - - For more details on the ordering of semantic versioning, see 'https://www.semver.org'. - ''' - parse_result = urllib.parse.urlparse(github_repository_url) - - if not parse_result.netloc: - raise ValueError(f'Could not determine host for github-url {github_repository_url}') - host = parse_result.netloc - - try: - path = parse_result.path.strip('/') - org, repo = path.split('/') - except ValueError as e: - raise ValueError(f"Could not extract org- and repo-name. Error: {e}") - - if anonymous: - if 'github.com' not in host: - raise ValueError("Anonymous access is only possible for github.com") - github_api = github3.GitHub() - repo_helper = GitHubRepositoryHelper(owner=org, name=repo, github_api=github_api) - - else: - repo_helper = ccc.github.repo_helper(host=host, org=org, repo=repo) - - print( - find_greatest_github_release_version( - releases=repo_helper.repository.releases(), - warn_for_unparseable_releases=False, - ignore_prerelease_versions=ignore_prereleases, - ) - ) diff --git a/concourse/steps/draft_release.mako b/concourse/steps/draft_release.mako index 85e4abb18..d46a465af 100644 --- a/concourse/steps/draft_release.mako +++ b/concourse/steps/draft_release.mako @@ -148,7 +148,7 @@ else: logger.info('draft release notes are already up to date') logger.info("Checking for outdated draft releases to delete") -for release, deletion_successful in github_helper.delete_outdated_draft_releases(): +for release, deletion_successful in github.release.delete_outdated_draft_releases(repository): if deletion_successful: logger.info(f"Deleted release '{release.name}'") else: diff --git a/concourse/steps/release.mako b/concourse/steps/release.mako index 8b5fdd304..ee2873484 100644 --- a/concourse/steps/release.mako +++ b/concourse/steps/release.mako @@ -463,10 +463,13 @@ except: pass % if release_trait.release_on_github(): +repo = github_helper.repository try: - clean_draft_releases( - github_helper=github_helper, - ) + for releases, succeeded in github.release.delete_outdated_draft_releases(repo): + if succeeded: + logger.info(f'deleted {release.name=}') + else: + logger.warn(f'failed to delete {release.name=}') except: logger.warning('An Error occurred whilst trying to remove draft-releases') traceback.print_exc() @@ -491,7 +494,6 @@ except: release_notes_md = None % endif -repo = github_helper.repository release_tag = tags[0].removeprefix('refs/tags') draft_tag = f'{version_str}-draft' diff --git a/concourse/steps/release.py b/concourse/steps/release.py index 323d682c7..679defcd4 100644 --- a/concourse/steps/release.py +++ b/concourse/steps/release.py @@ -27,9 +27,6 @@ import slackclient.util from gitutil import GitHelper -from github.util import ( - GitHubRepositoryHelper, -) from concourse.model.traits.release import ( ReleaseCommitPublishingPolicy, ) @@ -419,16 +416,6 @@ def upload_component_descriptor_as_release_asset( logger.warning('Unable to attach component-descriptors to release as release-asset.') -def clean_draft_releases( - github_helper: GitHubRepositoryHelper, -): - for release, deletion_successful in github_helper.delete_outdated_draft_releases(): - if deletion_successful: - logger.info(f'Deleted draft {release.name=}') - else: - logger.warning(f'Could not delete draft {release.name=}') - - def post_to_slack( release_notes_markdown, component: ocm.Component, diff --git a/github/release.py b/github/release.py index 81f9dcba5..b12887479 100644 --- a/github/release.py +++ b/github/release.py @@ -2,10 +2,16 @@ utils wrapping github3.py's relase-API ''' +import collections.abc as typehints +import logging + import github3.repos import github3.repos.release import github.limits +import version + +logger = logging.getLogger(__name__) def body_or_replacement( @@ -53,3 +59,134 @@ def find_draft_release( continue if release.name == name: return release + + +def delete_outdated_draft_releases( + repository: github3.repos.Repository, +) -> typehints.Generator[tuple[github3.repos.release.Release, bool], None, None]: + '''Find outdated draft releases and try to delete them + + Yields tuples containing a release and a boolean indicating whether its deletion was + successful. + + A draft release is considered outdated iff: + 1: its version is smaller than the greatest release version (according to semver) AND + 2a: it is NOT a hotfix draft release AND + 2b: there are no hotfix draft releases with the same major and minor version + OR + 3a: it is a hotfix draft release AND + 3b: there is a hotfix draft release of greater version (according to semver) + with the same major and minor version + ''' + + releases = [release for release in repository.releases(number=20)] + non_draft_releases = [release for release in releases if not release.draft] + draft_releases = [release for release in releases if release.draft] + greatest_release_version = find_greatest_github_release_version(non_draft_releases) + + if greatest_release_version is not None: + draft_releases_to_delete = outdated_draft_releases( + draft_releases=draft_releases, + greatest_release_version=greatest_release_version, + ) + else: + draft_releases_to_delete = [] + + for release in draft_releases_to_delete: + yield release, release.delete() + + +def outdated_draft_releases( + draft_releases: list[github3.repos.release.Release], + greatest_release_version: str, +): + '''Find outdated draft releases from a list of draft releases and return them. This is achieved + by partitioning the release versions according to their joined major and minor version. + Partitions are then checked: + - if there is only a single release in a partition it is either a hotfix release + (keep corresponding release) or it is not (delete if it is not the greatest release + according to semver) + - if there are multiple releases versions in a partition, keep only the release + corresponding to greatest (according to semver) + ''' + + greatest_release_version_info = version.parse_to_semver(greatest_release_version) + + def _has_semver_draft_prerelease_label(release_name): + version_info = version.parse_to_semver(release_name) + if version_info.prerelease != 'draft': + return False + return True + + autogenerated_draft_releases = [ + release for release in draft_releases + if release.name + and version.is_semver_parseable(release.name) + and _has_semver_draft_prerelease_label(release.name) + ] + + draft_release_version_infos = [ + version.parse_to_semver(release.name) + for release in autogenerated_draft_releases + ] + + def _yield_outdated_version_infos_from_partition(partition): + if len(partition) == 1: + version_info = partition.pop() + if version_info < greatest_release_version_info and version_info.patch == 0: + yield version_info + else: + yield from [ + version_info + for version_info in partition[1:] + ] + + outdated_version_infos = list() + for partition in version.partition_by_major_and_minor(draft_release_version_infos): + outdated_version_infos.extend(_yield_outdated_version_infos_from_partition(partition)) + + outdated_draft_releases = [ + release + for release in autogenerated_draft_releases + if version.parse_to_semver(release.name) in outdated_version_infos + ] + + return outdated_draft_releases + + +def find_greatest_github_release_version( + releases: list[github3.repos.release.Release], + warn_for_unparseable_releases: bool = True, + ignore_prerelease_versions: bool = False, +): + # currently, non-draft-releases are not created with a name by us. Use the tag name as fallback + release_versions = [ + release.name if release.name else release.tag_name + for release in releases + ] + + def filter_non_semver_parseable_releases(release_name): + try: + version.parse_to_semver(release_name) + return True + except ValueError: + if warn_for_unparseable_releases: + logger.warning(f'ignoring release {release_name=} (not semver)') + return False + + release_versions = [ + name for name in filter(filter_non_semver_parseable_releases, release_versions) + ] + + release_version_infos = [ + version.parse_to_semver(release_version) + for release_version in release_versions + ] + latest_version = version.find_latest_version( + versions=release_version_infos, + ignore_prerelease_versions=ignore_prerelease_versions, + ) + if latest_version: + return str(latest_version) + else: + return None diff --git a/github/util.py b/github/util.py index d3df965b7..df1f75f02 100644 --- a/github/util.py +++ b/github/util.py @@ -7,10 +7,8 @@ import enum import logging import re -import textwrap import typing -from typing import Iterable, Tuple import github3 import github3.issues @@ -18,7 +16,6 @@ from github3.github import GitHub from github3.orgs import Team from github3.pulls import PullRequest -from github3.repos.release import Release import ci.util import ocm @@ -26,15 +23,6 @@ logger = logging.getLogger(__name__) -''' -The maximum allowed length of github-release-bodies. -This limit is not documented explicitly in the GitHub docs. To see it, the error returned by -GitHub when creating a release with more then the allowed number of characters must be -looked at. -as (inofficial) alternative, see: https://github.com/dead-claudia/github-limits -''' -MAXIMUM_GITHUB_RELEASE_BODY_LENGTH = 25000 - class RepositoryHelperBase: GITHUB_TIMESTAMP_UTC_FORMAT = '%Y-%m-%dT%H:%M:%SZ' @@ -335,17 +323,6 @@ def has_upgrade_pr_title(pull_request): class GitHubRepositoryHelper(RepositoryHelperBase): - def _replacement_release_notes( - self, - asset_url: str, - ): - return textwrap.dedent( - f'''\ - Release-Notes were too long (limit: {MAXIMUM_GITHUB_RELEASE_BODY_LENGTH} octets). - They were instaed uploaded as [release-asset]({asset_url}). - ''' - ) - def tag_exists( self, tag_name: str, @@ -390,37 +367,7 @@ def is_team_member(self, team_name, user_login) -> bool: else: return True - def delete_outdated_draft_releases(self) -> Iterable[Tuple[github3.repos.release.Release, bool]]: - '''Find outdated draft releases and try to delete them - - Yields tuples containing a release and a boolean indicating whether its deletion was - successful. - A draft release is considered outdated iff: - 1: its version is smaller than the greatest release version (according to semver) AND - 2a: it is NOT a hotfix draft release AND - 2b: there are no hotfix draft releases with the same major and minor version - OR - 3a: it is a hotfix draft release AND - 3b: there is a hotfix draft release of greater version (according to semver) - with the same major and minor version - ''' - - releases = [release for release in self.repository.releases(number=20)] - non_draft_releases = [release for release in releases if not release.draft] - draft_releases = [release for release in releases if release.draft] - greatest_release_version = find_greatest_github_release_version(non_draft_releases) - - if greatest_release_version is not None: - draft_releases_to_delete = outdated_draft_releases( - draft_releases=draft_releases, - greatest_release_version=greatest_release_version, - ) - else: - draft_releases_to_delete = [] - - for release in draft_releases_to_delete: - yield release, release.delete() def _retrieve_team_by_name_or_none( @@ -432,102 +379,6 @@ def _retrieve_team_by_name_or_none( return team_list[0] if team_list else None -def find_greatest_github_release_version( - releases: list[Release], - warn_for_unparseable_releases: bool = True, - ignore_prerelease_versions: bool = False, -): - # currently, non-draft-releases are not created with a name by us. Use the tag name as fallback - release_versions = [ - release.name if release.name else release.tag_name - for release in releases - ] - - def filter_non_semver_parseable_releases(release_name): - try: - version.parse_to_semver(release_name) - return True - except ValueError: - if warn_for_unparseable_releases: - ci.util.warning(f'ignoring release {release_name=} (not semver)') - return False - - release_versions = [ - name for name in filter(filter_non_semver_parseable_releases, release_versions) - ] - - release_version_infos = [ - version.parse_to_semver(release_version) - for release_version in release_versions - ] - latest_version = version.find_latest_version( - versions=release_version_infos, - ignore_prerelease_versions=ignore_prerelease_versions, - ) - if latest_version: - return str(latest_version) - else: - return None - - -def outdated_draft_releases( - draft_releases: list[Release], - greatest_release_version: str, -): - '''Find outdated draft releases from a list of draft releases and return them. This is achieved - by partitioning the release versions according to their joined major and minor version. - Partitions are then checked: - - if there is only a single release in a partition it is either a hotfix release - (keep corresponding release) or it is not (delete if it is not the greatest release - according to semver) - - if there are multiple releases versions in a partition, keep only the release - corresponding to greatest (according to semver) - ''' - - greatest_release_version_info = version.parse_to_semver(greatest_release_version) - - def _has_semver_draft_prerelease_label(release_name): - version_info = version.parse_to_semver(release_name) - if version_info.prerelease != 'draft': - return False - return True - - autogenerated_draft_releases = [ - release for release in draft_releases - if release.name - and version.is_semver_parseable(release.name) - and _has_semver_draft_prerelease_label(release.name) - ] - - draft_release_version_infos = [ - version.parse_to_semver(release.name) - for release in autogenerated_draft_releases - ] - - def _yield_outdated_version_infos_from_partition(partition): - if len(partition) == 1: - version_info = partition.pop() - if version_info < greatest_release_version_info and version_info.patch == 0: - yield version_info - else: - yield from [ - version_info - for version_info in partition[1:] - ] - - outdated_version_infos = list() - for partition in version.partition_by_major_and_minor(draft_release_version_infos): - outdated_version_infos.extend(_yield_outdated_version_infos_from_partition(partition)) - - outdated_draft_releases = [ - release - for release in autogenerated_draft_releases - if version.parse_to_semver(release.name) in outdated_version_infos - ] - - return outdated_draft_releases - - def close_issue( issue: github3.issues.ShortIssue, ) -> bool: