def _run_task(self): repo = self.get_repo() version = self.options["version"] self.tag_name = self.project_config.get_tag_for_version(version) for release in repo.iter_releases(): if release.tag_name == self.tag_name: message = "Release {} already exists at {}".format( release.name, release.html_url) self.logger.error(message) raise GithubException(message) commit = self.options.get("commit", self.project_config.repo_commit) if not commit: message = "Could not detect the current commit from the local repo" self.logger.error(message) raise GithubException(message) ref = repo.ref("tags/{}".format(self.tag_name)) if not ref: # Create the annotated tag tag = repo.create_tag( tag=self.tag_name, message="Release of version {}".format(version), sha=commit, obj_type="commit", tagger={ "name": self.github_config.username, "email": self.github_config.email, "date": "{}Z".format(datetime.utcnow().isoformat()), }, lightweight=False, ) # Get the ref created from the previous call that for some reason creates # a ref to the commit sha rather than the tag sha. Delete the ref so we # can create the right one. FIXME: Is this a bug in github3.py? ref = repo.ref("tags/{}".format(self.tag_name)) if ref: ref.delete() # Create the ref linking to the tag ref = repo.create_ref(ref="refs/tags/{}".format(self.tag_name), sha=tag.sha) # Sleep for Github to catch up with the fact that the tag actually exists! time.sleep(3) prerelease = "Beta" in version # Create the Github Release release = repo.create_release(tag_name=self.tag_name, name=version, prerelease=prerelease) self.return_values = {"tag_name": self.tag_name, "name": version} self.logger.info("Created release {} at {}".format( release.name, release.html_url))
def _create_release(self, path, commit): # Get current release info repo = self.get_repo() # Get the ref try: ref = repo.ref(f"tags/{self.tag_name}") except github3.exceptions.NotFoundError: message = f"Ref not found for tag {self.tag_name}" raise GithubException(message) # Get the tag try: tag = repo.tag(ref.object.sha) except github3.exceptions.NotFoundError: message = f"Tag {self.tag_name} not found" raise GithubException(message) # Get the release try: release = repo.release_from_tag(self.tag_name) release_body = release.body if self.options["release_body"] else "" except github3.exceptions.NotFoundError: message = f"Release for {self.tag_name} not found" raise GithubException(message) # Check for tag in target repo try: self.target_repo.ref(f"tags/{self.tag_name}") except github3.exceptions.NotFoundError: pass else: message = f"Ref for tag {self.tag_name} already exists in target repo" raise GithubException(message) # Create the tag self.target_repo.create_tag( tag=self.tag_name, message=tag.message, sha=commit, obj_type="commit", tagger={ "name": self.github_config.username, "email": self.github_config.email, "date": "{}Z".format(datetime.utcnow().isoformat()), }, lightweight=False, ) # Create the release self.target_repo.create_release( tag_name=self.tag_name, name=self.options["version"], prerelease=release.prerelease, body=release_body, )
def _run_task(self): # args branch = self.options.get( 'branch', self.project_config.project__apexdoc__branch, ) if not branch: raise GithubException('Unable to determine branch name') if 'dir_local' in self.options: local_dir = self.options['dir_local'] else: local_base_dir = ( self.project_config.project__apexdoc__dir if self.project_config.project__apexdoc__dir else self.project_config.repo_root ) local_dir = os.path.join(local_base_dir, 'ApexDocumentation') repo_dir = self.options.get( 'dir_repo', self.project_config.project__apexdoc__repo_dir, ) dry_run = process_bool_arg(self.options.get('dry_run', False)) commit_message = self.options.get('commit_message', 'Update Apex docs') # get API repo = self.get_repo() # commit author = { 'name': self.github.user().name, 'email': self.github_config.email, } commit_dir = CommitDir(repo, self.logger, author) commit_dir(local_dir, branch, repo_dir, commit_message, dry_run)
def get_github_api_for_repo(keychain, owner, repo): gh = GitHub() # Apply retry policy gh.session.mount("http://", adapter) gh.session.mount("https://", adapter) APP_KEY = os.environ.get("GITHUB_APP_KEY", "").encode("utf-8") APP_ID = os.environ.get("GITHUB_APP_ID") if APP_ID and APP_KEY: installation = INSTALLATIONS.get((owner, repo)) if installation is None: gh.login_as_app(APP_KEY, APP_ID, expire_in=120) try: installation = gh.app_installation_for_repository(owner, repo) except github3.exceptions.NotFoundError: raise GithubException( "Could not access {}/{} using GitHub app. " "Does the app need to be installed for this repository?". format(owner, repo)) INSTALLATIONS[(owner, repo)] = installation gh.login_as_app_installation(APP_KEY, APP_ID, installation.id) else: github_config = keychain.get_service("github") gh.login(github_config.username, github_config.password) return gh
def _get_latest_tag_for_prefix(self, repo, prefix): for release in repo.releases(): if not release.tag_name.startswith(prefix): continue return release.tag_name raise GithubException( f"No release found for {self.repo_url} with tag prefix {prefix}")
def _run_task(self): repo = self.get_repo() ref = repo.ref("tags/{}".format(self.options["src_tag"])) try: src_tag = repo.tag(ref.object.sha) except github3.exceptions.NotFoundError: message = "Tag {} not found".format(self.options["src_tag"]) self.logger.error(message) raise GithubException(message) tag = repo.create_tag( tag=self.options["tag"], message="Cloned from {}".format(self.options["src_tag"]), sha=src_tag.sha, obj_type="commit", tagger={ "name": self.github_config.username, "email": self.github_config.email, "date": "{}Z".format(datetime.utcnow().isoformat()), }, ) self.logger.info("Tag {} created by cloning {}".format( self.options["tag"], self.options["src_tag"])) return tag
def _run_task(self): # args branch = self.options.get( "branch", self.project_config.project__apexdoc__branch ) if not branch: raise GithubException("Unable to determine branch name") local_dir = self.options.get("dir_local") if not local_dir: local_base_dir = ( self.project_config.project__apexdoc__dir if self.project_config.project__apexdoc__dir else self.project_config.repo_root ) local_dir = os.path.join(local_base_dir, "ApexDocumentation") repo_dir = self.options.get( "dir_repo", self.project_config.project__apexdoc__repo_dir ) dry_run = process_bool_arg(self.options.get("dry_run", False)) commit_message = self.options.get("commit_message", "Update Apex docs") # get API repo = self.get_repo() # commit author = {"name": self.github.user().name, "email": self.github_config.email} commit_dir = CommitDir(repo, self.logger, author) commit_dir(local_dir, branch, repo_dir, commit_message, dry_run)
def get_github_api_for_repo(keychain, owner, repo, session=None): gh = GitHub( session=session or GitHubSession(default_read_timeout=30, default_connect_timeout=30) ) # Apply retry policy gh.session.mount("http://", adapter) gh.session.mount("https://", adapter) GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN") APP_KEY = os.environ.get("GITHUB_APP_KEY", "").encode("utf-8") APP_ID = os.environ.get("GITHUB_APP_ID") if APP_ID and APP_KEY: installation = INSTALLATIONS.get((owner, repo)) if installation is None: gh.login_as_app(APP_KEY, APP_ID, expire_in=120) try: installation = gh.app_installation_for_repository(owner, repo) except github3.exceptions.NotFoundError: raise GithubException( f"Could not access {owner}/{repo} using GitHub app. " "Does the app need to be installed for this repository?" ) INSTALLATIONS[(owner, repo)] = installation gh.login_as_app_installation(APP_KEY, APP_ID, installation.id) elif GITHUB_TOKEN: gh.login(token=GITHUB_TOKEN) else: github_config = keychain.get_service("github") gh.login(github_config.username, github_config.password) return gh
def _get_repo(self): gh = self.get_github_api() repo = gh.repository(self.repo_owner, self.repo_name) if repo is None: raise GithubException( f"Github repository not found or not authorized. ({self.repo_url})" ) return repo
def _update_head(self, new_commit): if self.dry_run: self.logger.info("[dry_run] Skipping call to update HEAD") else: self.logger.info("Updating HEAD") success = self.head.update(new_commit.sha) if not success: raise GithubException("Failed to update HEAD")
def validate_service(options): username = options["username"] password = options["password"] gh = get_github_api(username, password) try: gh.rate_limit() except Exception as e: raise GithubException(f"Could not confirm access to the GitHub API: {str(e)}")
def _create_tree(self, new_tree_list): new_tree = None if self.dry_run: self.logger.info("[dry_run] Skipping creation of new tree") else: self.logger.info("Creating new tree") new_tree = self.repo.create_tree(new_tree_list, None) if not new_tree: raise GithubException("Failed to create tree") return new_tree
def _create_blob(self, content): try: content = content.decode('utf-8') blob_sha = self.repo.create_blob(content, 'utf-8') except UnicodeDecodeError: content = base64.b64encode(content) blob_sha = self.repo.create_blob(content, 'base64') if not blob_sha: raise GithubException('Failed to create blob') return blob_sha
def _create_blob(self, content): try: content = content.decode("utf-8") blob_sha = self.repo.create_blob(content, "utf-8") except UnicodeDecodeError: content = base64.b64encode(content) blob_sha = self.repo.create_blob(content, "base64") if not blob_sha: raise GithubException("Failed to create blob") return blob_sha
def _init_options(self, kwargs): super()._init_options(kwargs) self.commit = self.options.get("commit", self.project_config.repo_commit) if not self.commit: message = "Could not detect the current commit from the local repo" self.logger.error(message) raise GithubException(message) if len(self.commit) != 40: raise TaskOptionsError("The commit option must be exactly 40 characters.")
def _validate_dirs(self, local_dir, repo_dir): local_dir = os.path.abspath(local_dir) if not os.path.isdir(local_dir): raise GithubException("Not a dir: {}".format(local_dir)) # do not use os.path because repo_dir is not local if repo_dir is None: repo_dir = "" if repo_dir.startswith("."): repo_dir = repo_dir[1:] if repo_dir.startswith("/"): repo_dir = repo_dir[1:] if repo_dir.endswith("/"): repo_dir = repo_dir[:-1] return local_dir, repo_dir
def get_latest_tag(self, beta=False): """ Query Github Releases to find the latest production or beta tag """ repo = self._get_repo() if not beta: try: release = repo.latest_release() except github3.exceptions.NotFoundError: raise GithubException(f"No release found for repo {self.repo_url}") prefix = self.project__git__prefix_release if not release.tag_name.startswith(prefix): return self._get_latest_tag_for_prefix(repo, prefix) return release.tag_name else: return self._get_latest_tag_for_prefix(repo, self.project__git__prefix_beta)
def _create_commit(self, commit_message, new_tree): if commit_message is None: commit_message = "Commit dir {} to {}/{}/{} via CumulusCI".format( self.local_dir, self.repo.owner, self.repo.name, self.repo_dir) new_commit = None if self.dry_run: self.logger.info("[dry_run] Skipping creation of new commit") else: self.logger.info("Creating new commit") new_commit = self.repo.create_commit( message=commit_message, tree=new_tree.sha, parents=[self.parent_commit.sha], author=self.author, committer=self.author, ) if not new_commit: raise GithubException("Failed to create commit") return new_commit
def _create_blob(self, content, local_file): if self.dry_run: self.logger.info("[dry_run] Skipping creation of " + "blob for new file: {}".format(local_file)) blob_sha = None else: self.logger.info( "Creating blob for new file: {}".format(local_file)) try: content = content.decode("utf-8") blob_sha = self.repo.create_blob(content, "utf-8") except UnicodeDecodeError: content = base64.b64encode(content) blob_sha = self.repo.create_blob(content.decode("utf-8"), "base64") if not blob_sha: raise GithubException("Failed to create blob") self.logger.debug("Blob created: {}".format(blob_sha)) return blob_sha
def _create_commit(self, commit_message, new_tree): if commit_message is None: commit_message = "Commit dir {} to {}/{}/{} via CumulusCI".format( self.local_dir, self.repo.owner, self.repo.name, self.repo_dir) new_commit = None if self.dry_run: self.logger.info("[dry_run] Skipping creation of new commit") else: self.logger.info("Creating new commit") commit_info = { "message": commit_message, "tree": new_tree.sha, "parents": [self.parent_commit.sha], } if self.author: commit_info["author"] = self.author commit_info["committer"] = self.author new_commit = self.repo.create_commit(**commit_info) if not new_commit: raise GithubException("Failed to create commit") return new_commit
def _run_task(self): repo = self.get_repo() ref = repo.ref('tags/{}'.format(self.options['src_tag'])) src_tag = repo.tag(ref.object.sha) if not src_tag: message = 'Tag {} not found'.format(self.options['src_tag']) logger.error(message) raise GithubException(message) tag = repo.create_tag( tag = self.options['tag'], message = 'Cloned from {}'.format(self.options['src_tag']), sha = src_tag.sha, obj_type = 'commit', tagger = { 'name': self.github_config.username, 'email': self.github_config.email, 'date': '{}Z'.format(datetime.utcnow().isoformat()), }, ) self.logger.info('Tag {} created by cloning {}'.format(self.options['tag'], self.options['src_tag'])) return tag
def __call__(self, local_dir, branch, repo_dir=None, commit_message=None, dry_run=False, ): """ local_dir: path to local directory to commit branch: target branch name repo_dir: target path within repo - use '' for repo root commit_message: message for git commit dry_run: skip creating GitHub data if True """ # prepare dir args local_dir = os.path.abspath(local_dir) if not os.path.isdir(local_dir): raise GithubException('Not a dir: {}'.format(local_dir)) # do not use os.path because repo_dir is not local if repo_dir is None: repo_dir = '' if repo_dir.startswith('.'): repo_dir = repo_dir[1:] if repo_dir.startswith('/'): repo_dir = repo_dir[1:] if repo_dir.endswith('/'): repo_dir = repo_dir[:-1] # get ref to branch HEAD head = self.repo.ref('heads/{}'.format(branch)) # get commit pointed to by HEAD commit = self.repo.git_commit(head.object.sha) # get tree of commit tree = self.repo.tree(commit.tree.sha) # shallow tree tree = tree.recurse() # recurse/flatten tree = tree.to_json() # convert to native types # create new tree (delete, update) new_tree_list = [] for item in tree['tree']: if item['type'] == 'tree': # remove sub-trees as they are implied by blob paths self.logger.debug('Removing tree: {}'.format(item['path'])) continue if not item['path'].startswith(repo_dir): # outside target dir in repo - keep in tree self.logger.debug( 'Unchanged (outside target path): {}'.format(item['path']) ) new_tree_list.append(item) continue len_path = (len(repo_dir) + 1) if repo_dir else 0 item_subpath = item['path'][len_path:] local_file = os.path.join(local_dir, item_subpath) if not os.path.isfile(local_file): # delete blob from tree self.logger.debug('Delete: {}'.format(item['path'])) continue with io.open(local_file, 'rb') as f: content = f.read() header = 'blob {}\0'.format(len(content)) sha = hashlib.sha1(header + content).hexdigest() new_item = item.copy() if sha != item['sha']: self.logger.debug('Update: {}'.format(local_file)) if dry_run: self.logger.info('[dry_run] Skipping creation of blob ' + 'for file: {}'.format(local_file) ) blob_sha = None else: self.logger.info( 'Creating blob for updated file: {}'.format(local_file) ) blob_sha = self._create_blob(content) self.logger.debug('Blob created: {}'.format(blob_sha)) new_item['sha'] = blob_sha else: self.logger.debug('Unchanged: {}'.format(item['path'])) new_tree_list.append(new_item) # add new files to tree new_tree_target_subpaths = [] for item in new_tree_list: if not item['path'].startswith(repo_dir): # skip items not in target dir continue len_path = (len(repo_dir) + 1) if repo_dir else 0 new_tree_target_subpaths.append(item['path'][len_path:]) for root, dirs, files in os.walk(local_dir): for filename in files: if filename.startswith('.'): # skip hidden files continue local_file = os.path.join(root, filename) local_file_subpath = local_file[(len(local_dir) + 1):] if local_file_subpath not in new_tree_target_subpaths: with io.open(local_file, 'rb') as f: content = f.read() if dry_run: self.logger.info('[dry_run] Skipping creation of ' + 'blob for new file: {}'.format(local_file) ) blob_sha = None else: self.logger.info( 'Creating blob for new file: {}'.format(local_file) ) blob_sha = self._create_blob(content) self.logger.debug('Blob created: {}'.format(blob_sha)) repo_path = (repo_dir + '/') if repo_dir else '' new_item = { 'path': '{}{}'.format(repo_path, local_file_subpath), 'mode': '100644', 'type': 'blob', 'sha': blob_sha, } new_tree_list.append(new_item) # generate summary of changes self.logger.info('Summary of changes:') new_shas = [item['sha'] for item in new_tree_list] new_paths = [item['path'] for item in new_tree_list] old_paths = [item['path'] for item in tree['tree']] old_tree_list = [] for item in tree['tree']: if item['type'] == 'tree': continue old_tree_list.append(item) if item['path'] not in new_paths: self.logger.warning('Delete:\t{}'.format(item['path'])) elif item['sha'] not in new_shas: self.logger.info('Update:\t{}'.format(item['path'])) for item in new_tree_list: if item['path'] not in old_paths: self.logger.info('Add:\t{}'.format(item['path'])) if new_tree_list == old_tree_list: self.logger.warning('No changes found, aborting commit') return # create new tree if dry_run: self.logger.info('[dry_run] Skipping creation of new tree') else: self.logger.info('Creating new tree') new_tree = self.repo.create_tree(new_tree_list, None) if not new_tree: raise GithubException('Failed to create tree') # create new commit if commit_message is None: commit_message = 'Commit dir {} to {}/{}/{} via CumulusCI'.format( local_dir, self.repo.owner, self.repo.name, repo_dir, ) if dry_run: self.logger.info('[dry_run] Skipping creation of new commit') else: self.logger.info('Creating new commit') new_commit = self.repo.create_commit( message=commit_message, tree=new_tree.sha, parents=[commit.sha], author=self.author, committer=self.author, ) if not new_commit: raise GithubException('Failed to create commit') # update HEAD if dry_run: self.logger.info('[dry_run] Skipping call to update HEAD') else: self.logger.info('Updating HEAD') success = head.update(new_commit.sha) if not success: raise GithubException('Failed to update HEAD')
def _run_task(self): repo = self.get_repo() version = self.options["version"] tag_name = self.project_config.get_tag_for_version(version) # Make sure release doesn't already exist try: release = repo.release_from_tag(tag_name) except github3.exceptions.NotFoundError: pass else: message = "Release {} already exists at {}".format( release.name, release.html_url ) self.logger.error(message) raise GithubException(message) # Build tag message message = self.options.get("message", "Release of version {}".format(version)) dependencies = self.project_config.get_static_dependencies( self.options.get("dependencies") or self.project_config.project__dependencies ) if dependencies: message += "\n\ndependencies: {}".format(json.dumps(dependencies, indent=4)) try: repo.ref("tags/{}".format(tag_name)) except github3.exceptions.NotFoundError: # Create the annotated tag repo.create_tag( tag=tag_name, message=message, sha=self.commit, obj_type="commit", tagger={ "name": self.github_config.username, "email": self.github_config.email, "date": "{}Z".format(datetime.utcnow().isoformat()), }, lightweight=False, ) # Sleep for Github to catch up with the fact that the tag actually exists! time.sleep(3) prerelease = "Beta" in version # Create the Github Release release = repo.create_release( tag_name=tag_name, name=version, prerelease=prerelease ) self.return_values = { "tag_name": tag_name, "name": version, "dependencies": dependencies, } self.logger.info( "Created release {} at {}".format(release.name, release.html_url) )
def __call__(self, local_dir, branch, repo_dir=None, commit_message=None, dry_run=False): """ local_dir: path to local directory to commit branch: target branch name repo_dir: target path within repo - use '' for repo root commit_message: message for git commit dry_run: skip creating GitHub data if True """ # prepare dir args local_dir, repo_dir = self._validate_dirs(local_dir, repo_dir) # get ref to branch HEAD head = self.repo.ref("heads/{}".format(branch)) # get commit pointed to by HEAD commit = self.repo.git_commit(head.object.sha) # get tree of commit tree = self.repo.tree(commit.tree.sha) # shallow tree tree = tree.recurse() # recurse/flatten tree = tree.to_json() # convert to native types # create new tree (delete, update) new_tree_list = [] for item in tree["tree"]: if item["type"] == "tree": # remove sub-trees as they are implied by blob paths self.logger.debug("Removing tree: {}".format(item["path"])) continue if not item["path"].startswith(repo_dir): # outside target dir in repo - keep in tree self.logger.debug("Unchanged (outside target path): {}".format( item["path"])) new_tree_list.append(item) continue len_path = (len(repo_dir) + 1) if repo_dir else 0 item_subpath = item["path"][len_path:] local_file = os.path.join(local_dir, item_subpath) if not os.path.isfile(local_file): # delete blob from tree self.logger.debug("Delete: {}".format(item["path"])) continue with io.open(local_file, "rb") as f: content = f.read() header = b"blob " + str(len(content)).encode() + b"\0" sha = hashlib.sha1(header + content).hexdigest() new_item = item.copy() if sha != item["sha"]: self.logger.debug("Update: {}".format(local_file)) if dry_run: self.logger.info("[dry_run] Skipping creation of blob " + "for file: {}".format(local_file)) blob_sha = None else: self.logger.info( "Creating blob for updated file: {}".format( local_file)) blob_sha = self._create_blob(content) self.logger.debug("Blob created: {}".format(blob_sha)) new_item["sha"] = blob_sha else: self.logger.debug("Unchanged: {}".format(item["path"])) new_tree_list.append(new_item) # add new files to tree new_tree_target_subpaths = [] for item in new_tree_list: if not item["path"].startswith(repo_dir): # skip items not in target dir continue len_path = (len(repo_dir) + 1) if repo_dir else 0 new_tree_target_subpaths.append(item["path"][len_path:]) for root, dirs, files in os.walk(local_dir): for filename in files: if filename.startswith("."): # skip hidden files continue local_file = os.path.join(root, filename) local_file_subpath = local_file[(len(local_dir) + 1):] if local_file_subpath not in new_tree_target_subpaths: with io.open(local_file, "rb") as f: content = f.read() if dry_run: self.logger.info( "[dry_run] Skipping creation of " + "blob for new file: {}".format(local_file)) blob_sha = None else: self.logger.info( "Creating blob for new file: {}".format( local_file)) blob_sha = self._create_blob(content) self.logger.debug("Blob created: {}".format(blob_sha)) repo_path = (repo_dir + "/") if repo_dir else "" new_item = { "path": "{}{}".format(repo_path, local_file_subpath), "mode": "100644", "type": "blob", "sha": blob_sha, } new_tree_list.append(new_item) # generate summary of changes self.logger.info("Summary of changes:") new_shas = [item["sha"] for item in new_tree_list] new_paths = [item["path"] for item in new_tree_list] old_paths = [item["path"] for item in tree["tree"]] old_tree_list = [] for item in tree["tree"]: if item["type"] == "tree": continue old_tree_list.append(item) if item["path"] not in new_paths: self.logger.warning("Delete:\t{}".format(item["path"])) elif item["sha"] not in new_shas: self.logger.info("Update:\t{}".format(item["path"])) for item in new_tree_list: if item["path"] not in old_paths: self.logger.info("Add:\t{}".format(item["path"])) if new_tree_list == old_tree_list: self.logger.warning("No changes found, aborting commit") return # create new tree if dry_run: self.logger.info("[dry_run] Skipping creation of new tree") else: self.logger.info("Creating new tree") new_tree = self.repo.create_tree(new_tree_list, None) if not new_tree: raise GithubException("Failed to create tree") # create new commit if commit_message is None: commit_message = "Commit dir {} to {}/{}/{} via CumulusCI".format( local_dir, self.repo.owner, self.repo.name, repo_dir) if dry_run: self.logger.info("[dry_run] Skipping creation of new commit") else: self.logger.info("Creating new commit") new_commit = self.repo.create_commit( message=commit_message, tree=new_tree.sha, parents=[commit.sha], author=self.author, committer=self.author, ) if not new_commit: raise GithubException("Failed to create commit") # update HEAD if dry_run: self.logger.info("[dry_run] Skipping call to update HEAD") else: self.logger.info("Updating HEAD") success = head.update(new_commit.sha) if not success: raise GithubException("Failed to update HEAD")
def _run_task(self): repo = self.get_repo() for release in repo.iter_releases(): if release.name == self.options['version']: message = 'Release {} already exists at {}'.format( release.name, release.html_url) self.logger.error(message) return GithubException(message) commit = self.options.get('commit', self.project_config.repo_commit) if not commit: message = 'Could not detect the current commit from the local repo' self.logger.error(message) return GithubException(message) version = self.options['version'] self.tag_name = self.project_config.get_tag_for_version(version) ref = repo.ref('tags/{}'.format(self.tag_name)) if not ref: # Create the annotated tag tag = repo.create_tag( tag=self.tag_name, message='Release of version {}'.format(version), sha=commit, obj_type='commit', tagger={ 'name': self.github_config.username, 'email': self.github_config.email, 'date': '{}Z'.format(datetime.now().isoformat()), }, lightweight=False, ) # Get the ref created from the previous call that for some reason creates # a ref to the commit sha rather than the tag sha. Delete the ref so we # can create the right one. FIXME: Is this a bug in github3.py? ref = repo.ref('tags/{}'.format(self.tag_name)) if ref: ref.delete() # Create the ref linking to the tag ref = repo.create_ref( ref='refs/tags/{}'.format(self.tag_name), sha=tag.sha, ) # Sleep for Github to catch up with the fact that the tag actually exists! time.sleep(3) prerelease = 'Beta' in version # Create the Github Release release = repo.create_release( tag_name=self.tag_name, name=version, prerelease=prerelease, ) self.return_values = {'tag_name': self.tag_name} self.logger.info('Created release {} at {}'.format( release.name, release.html_url))