123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245 |
- from __future__ import absolute_import
- from __future__ import unicode_literals
- import os
- import tempfile
- import requests
- from git import GitCommandError
- from git import Repo
- from github import Github
- from .const import NAME
- from .const import REPO_ROOT
- from .utils import branch_name
- from .utils import read_release_notes_from_changelog
- from .utils import ScriptError
- class Repository(object):
- def __init__(self, root=None, gh_name=None):
- if root is None:
- root = REPO_ROOT
- if gh_name is None:
- gh_name = NAME
- self.git_repo = Repo(root)
- self.gh_client = Github(os.environ['GITHUB_TOKEN'])
- self.gh_repo = self.gh_client.get_repo(gh_name)
- def create_release_branch(self, version, base=None):
- print('Creating release branch {} based on {}...'.format(version, base or 'master'))
- remote = self.find_remote(self.gh_repo.full_name)
- br_name = branch_name(version)
- remote.fetch()
- if self.branch_exists(br_name):
- raise ScriptError(
- "Branch {} already exists locally. Please remove it before "
- "running the release script, or use `resume` instead.".format(
- br_name
- )
- )
- if base is not None:
- base = self.git_repo.tag('refs/tags/{}'.format(base))
- else:
- base = 'refs/remotes/{}/master'.format(remote.name)
- release_branch = self.git_repo.create_head(br_name, commit=base)
- release_branch.checkout()
- self.git_repo.git.merge('--strategy=ours', '--no-edit', '{}/release'.format(remote.name))
- with release_branch.config_writer() as cfg:
- cfg.set_value('release', version)
- return release_branch
- def find_remote(self, remote_name=None):
- if not remote_name:
- remote_name = self.gh_repo.full_name
- for remote in self.git_repo.remotes:
- for url in remote.urls:
- if remote_name in url:
- return remote
- return None
- def create_bump_commit(self, bump_branch, version):
- print('Creating bump commit...')
- bump_branch.checkout()
- self.git_repo.git.commit('-a', '-s', '-m "Bump {}"'.format(version), '--no-verify')
- def diff(self):
- return self.git_repo.git.diff()
- def checkout_branch(self, name):
- return self.git_repo.branches[name].checkout()
- def push_branch_to_remote(self, branch, remote_name=None):
- print('Pushing branch {} to remote...'.format(branch.name))
- remote = self.find_remote(remote_name)
- remote.push(refspec=branch, force=True)
- def branch_exists(self, name):
- return name in [h.name for h in self.git_repo.heads]
- def create_release_pull_request(self, version):
- return self.gh_repo.create_pull(
- title='Bump {}'.format(version),
- body='Automated release for docker-compose {}\n\n{}'.format(
- version, read_release_notes_from_changelog()
- ),
- base='release',
- head=branch_name(version),
- )
- def create_release(self, version, release_notes, **kwargs):
- return self.gh_repo.create_git_release(
- tag=version, name=version, message=release_notes, **kwargs
- )
- def find_release(self, version):
- print('Retrieving release draft for {}'.format(version))
- releases = self.gh_repo.get_releases()
- for release in releases:
- if release.tag_name == version and release.title == version:
- return release
- return None
- def publish_release(self, release):
- release.update_release(
- name=release.title,
- message=release.body,
- draft=False,
- prerelease=release.prerelease
- )
- def remove_release(self, version):
- print('Removing release draft for {}'.format(version))
- releases = self.gh_repo.get_releases()
- for release in releases:
- if release.tag_name == version and release.title == version:
- if not release.draft:
- print(
- 'The release at {} is no longer a draft. If you TRULY intend '
- 'to remove it, please do so manually.'.format(release.url)
- )
- continue
- release.delete_release()
- def remove_bump_branch(self, version, remote_name=None):
- name = branch_name(version)
- if not self.branch_exists(name):
- return False
- print('Removing local branch "{}"'.format(name))
- if self.git_repo.active_branch.name == name:
- print('Active branch is about to be deleted. Checking out to master...')
- try:
- self.checkout_branch('master')
- except GitCommandError:
- raise ScriptError(
- 'Unable to checkout master. Try stashing local changes before proceeding.'
- )
- self.git_repo.branches[name].delete(self.git_repo, name, force=True)
- print('Removing remote branch "{}"'.format(name))
- remote = self.find_remote(remote_name)
- try:
- remote.push(name, delete=True)
- except GitCommandError as e:
- if 'remote ref does not exist' in str(e):
- return False
- raise ScriptError(
- 'Error trying to remove remote branch: {}'.format(e)
- )
- return True
- def find_release_pr(self, version):
- print('Retrieving release PR for {}'.format(version))
- name = branch_name(version)
- open_prs = self.gh_repo.get_pulls(state='open')
- for pr in open_prs:
- if pr.head.ref == name:
- print('Found matching PR #{}'.format(pr.number))
- return pr
- print('No open PR for this release branch.')
- return None
- def close_release_pr(self, version):
- print('Retrieving and closing release PR for {}'.format(version))
- name = branch_name(version)
- open_prs = self.gh_repo.get_pulls(state='open')
- count = 0
- for pr in open_prs:
- if pr.head.ref == name:
- print('Found matching PR #{}'.format(pr.number))
- pr.edit(state='closed')
- count += 1
- if count == 0:
- print('No open PR for this release branch.')
- return count
- def write_git_sha(self):
- with open(os.path.join(REPO_ROOT, 'compose', 'GITSHA'), 'w') as f:
- f.write(self.git_repo.head.commit.hexsha[:7])
- def cherry_pick_prs(self, release_branch, ids):
- if not ids:
- return
- release_branch.checkout()
- for i in ids:
- try:
- i = int(i)
- except ValueError as e:
- raise ScriptError('Invalid PR id: {}'.format(e))
- print('Retrieving PR#{}'.format(i))
- pr = self.gh_repo.get_pull(i)
- patch_data = requests.get(pr.patch_url).text
- self.apply_patch(patch_data)
- def apply_patch(self, patch_data):
- with tempfile.NamedTemporaryFile(mode='w', prefix='_compose_cherry', encoding='utf-8') as f:
- f.write(patch_data)
- f.flush()
- self.git_repo.git.am('--3way', f.name)
- def get_prs_in_milestone(self, version):
- milestones = self.gh_repo.get_milestones(state='open')
- milestone = None
- for ms in milestones:
- if ms.title == version:
- milestone = ms
- break
- if not milestone:
- print('Didn\'t find a milestone matching "{}"'.format(version))
- return None
- issues = self.gh_repo.get_issues(milestone=milestone, state='all')
- prs = []
- for issue in issues:
- if issue.pull_request is not None:
- prs.append(issue.number)
- return sorted(prs)
- def get_contributors(pr_data):
- commits = pr_data.get_commits()
- authors = {}
- for commit in commits:
- if not commit.author:
- continue
- author = commit.author.login
- authors[author] = authors.get(author, 0) + 1
- return [x[0] for x in sorted(list(authors.items()), key=lambda x: x[1])]
- def upload_assets(gh_release, files):
- print('Uploading binaries and hash sums')
- for filename, filedata in files.items():
- print('Uploading {}...'.format(filename))
- gh_release.upload_asset(filedata[0], content_type='application/octet-stream')
- gh_release.upload_asset('{}.sha256'.format(filedata[0]), content_type='text/plain')
- print('Uploading run.sh...')
- gh_release.upload_asset(
- os.path.join(REPO_ROOT, 'script', 'run', 'run.sh'), content_type='text/plain'
- )
- def delete_assets(gh_release):
- print('Removing previously uploaded assets')
- for asset in gh_release.get_assets():
- print('Deleting asset {}'.format(asset.name))
- asset.delete_asset()
|