def main() -> None: args = parse_args() repo = GitRepo(get_git_repo_dir(), get_git_remote_name()) org, project = repo.gh_owner_and_name() pr = GitHubPR(org, project, args.pr_num) def handle_exception(e: Exception, msg: str = "Merge failed") -> None: msg += f" due to {e}" run_url = os.getenv("GH_RUN_URL") if run_url is not None: msg += f"\nRaised by {run_url}" gh_post_comment(org, project, args.pr_num, msg, dry_run=args.dry_run) import traceback traceback.print_exc() if args.revert: try: try_revert(repo, pr, dry_run=args.dry_run, comment_id=args.comment_id) except Exception as e: handle_exception(e, f"Reverting PR {args.pr_num} failed") return if pr.is_closed(): gh_post_comment(org, project, args.pr_num, f"Can't merge closed PR #{args.pr_num}", dry_run=args.dry_run) return if pr.is_cross_repo() and pr.is_ghstack_pr(): gh_post_comment(org, project, args.pr_num, "Cross-repo ghstack merges are not supported", dry_run=args.dry_run) return if args.on_green: try: merge_on_green(args.pr_num, repo, dry_run=args.dry_run) except Exception as e: handle_exception(e) else: try: pr.merge_into(repo, dry_run=args.dry_run, force=args.force) except Exception as e: handle_exception(e)
def main() -> None: args = parse_args() repo = GitRepo(get_git_repo_dir(), get_git_remote_name(), debug=True) org, project = repo.gh_owner_and_name() run_url = os.environ.get("GH_RUN_URL") job_link = f"[job]({run_url})" if run_url is not None else "job" msg = ( f"The {args.action} {job_link} was canceled. If you believe this is a mistake," + f"then you can re trigger it through [pytorch-bot]({BOT_COMMANDS_WIKI})." ) gh_post_pr_comment(org, project, args.pr_num, msg) print(org, project, args.pr_num, msg)
def fetch_check_run_conclusions(repo: GitRepo, commit: str) -> Dict[str, Tuple[str, str]]: [owner, name] = repo.gh_owner_and_name() checks = fetch_json_dict( f'https://api.github.com/repos/{owner}/{name}/commits/{commit}/check-runs' ) check_run_conclusions = {} if len(checks['check_runs']) == 0: raise MandatoryChecksMissingError( "Refusing to merge as land check(s) are not yet run") for check_run in checks['check_runs']: check_run_conclusions[check_run['name']] = (check_run['conclusion'], check_run['html_url']) return check_run_conclusions
def merge(pr_num: int, repo: GitRepo, dry_run: bool = False, force: bool = False, comment_id: Optional[int] = None, mandatory_only: bool = False, on_green: bool = False, timeout_minutes: int = 400, stale_pr_days: int = 3) -> None: repo = GitRepo(get_git_repo_dir(), get_git_remote_name()) org, project = repo.gh_owner_and_name() pr = GitHubPR(org, project, pr_num) initial_commit_sha = pr.last_commit()['oid'] if force or can_skip_internal_checks(pr, comment_id): # do not wait for any pending signals if PR is closed as part of co-development process return pr.merge_into(repo, dry_run=dry_run, force=force, comment_id=comment_id) if (datetime.utcnow() - pr.last_pushed_at()).days > stale_pr_days: raise RuntimeError("This PR is too stale; the last push date was more than 3 days ago. Please rebase and try again.") start_time = time.time() last_exception = '' elapsed_time = 0.0 while elapsed_time < timeout_minutes * 60: current_time = time.time() elapsed_time = current_time - start_time print(f"Attempting merge of https://github.com/{org}/{project}/pull/{pr_num} ({elapsed_time / 60} minutes elapsed)") pr = GitHubPR(org, project, pr_num) if initial_commit_sha != pr.last_commit()['oid']: raise RuntimeError("New commits were pushed while merging. Please rerun the merge command.") try: find_matching_merge_rule(pr, repo) pending = pr_get_pending_checks(pr) failing = pr_get_failed_checks(pr) if (not mandatory_only and on_green) and len(failing) > 0: raise RuntimeError(f"{len(failing)} additional jobs have failed, first few of them are: " + ' ,'.join(f"[{x[0]}]({x[1]})" for x in failing[:5])) if (not mandatory_only and on_green) and len(pending) > 0: raise MandatoryChecksMissingError(f"Still waiting for {len(pending)} additional jobs to finish, " + f"first few of them are: {' ,'.join(x[0] for x in pending[:5])}") return pr.merge_into(repo, dry_run=dry_run, force=force, comment_id=comment_id) except MandatoryChecksMissingError as ex: last_exception = str(ex) print(f"Merge of https://github.com/{org}/{project}/pull/{pr_num} failed due to: {ex}. Retrying in 5 min") time.sleep(5 * 60) # Finally report timeout back msg = f"Merged timed out after {timeout_minutes} minutes. Please contact the pytorch_dev_infra team." msg += f"The last exception was: {last_exception}" if not dry_run: gh_add_labels(org, project, pr_num, ["land-failed"]) raise RuntimeError(msg)
def main() -> None: args = parse_args() repo = GitRepo(get_git_repo_dir(), get_git_remote_name()) org, project = repo.gh_owner_and_name() pr = GitHubPR(org, project, args.pr_num) if args.revert: try: try_revert(repo, pr, dry_run=args.dry_run) except Exception as e: msg = f"Reverting PR {args.pr_num} failed due to {e}" run_url = os.getenv("GH_RUN_URL") if run_url is not None: msg += f"\nRaised by {run_url}" gh_post_comment(org, project, args.pr_num, msg, dry_run=args.dry_run) return if pr.is_closed(): gh_post_comment(org, project, args.pr_num, f"Can't merge closed PR #{args.pr_num}", dry_run=args.dry_run) return if pr.is_cross_repo() and pr.is_ghstack_pr(): gh_post_comment(org, project, args.pr_num, "Cross-repo ghstack merges are not supported", dry_run=args.dry_run) return try: pr.merge_into(repo, dry_run=args.dry_run) except Exception as e: msg = f"Merge failed due to {e}" run_url = os.getenv("GH_RUN_URL") if run_url is not None: msg += f"\nRaised by {run_url}" gh_post_comment(org, project, args.pr_num, msg, dry_run=args.dry_run)
def main() -> None: args = parse_args() repo = GitRepo(get_git_repo_dir(), get_git_remote_name()) org, project = repo.gh_owner_and_name() pr = GitHubPR(org, project, args.pr_num) def handle_exception(e: Exception, msg: str = "Merge failed") -> None: msg += f" due to {e}" run_url = os.getenv("GH_RUN_URL") if run_url is not None: msg += f"\nRaised by {run_url}" gh_post_pr_comment(org, project, args.pr_num, msg, dry_run=args.dry_run) import traceback traceback.print_exc() msg = f"@pytorchbot successfully started a {'revert' if args.revert else 'merge'} job." msg += f" Check the current status [here]({os.getenv('GH_RUN_URL')})" gh_post_pr_comment(org, project, args.pr_num, msg, dry_run=args.dry_run) if args.revert: try: try_revert(repo, pr, dry_run=args.dry_run, comment_id=args.comment_id, reason=args.reason) except Exception as e: handle_exception(e, f"Reverting PR {args.pr_num} failed") return if pr.is_closed(): gh_post_pr_comment(org, project, args.pr_num, f"Can't merge closed PR #{args.pr_num}", dry_run=args.dry_run) return if pr.is_cross_repo() and pr.is_ghstack_pr(): gh_post_pr_comment(org, project, args.pr_num, "Cross-repo ghstack merges are not supported", dry_run=args.dry_run) return try: merge(args.pr_num, repo, dry_run=args.dry_run, force=args.force, comment_id=args.comment_id, on_green=args.on_green, mandatory_only=args.on_mandatory, land_checks=args.land_checks) except Exception as e: handle_exception(e)
def main() -> None: import sys args = parse_args() repo = GitRepo(get_git_repo_dir(), get_git_remote_name()) org, project = repo.gh_owner_and_name() pr = GitHubPR(org, project, args.pr_num) if pr.is_closed(): print(gh_post_comment(org, project, args.pr_num, f"Can't merge closed PR #{args.pr_num}", dry_run=args.dry_run)) sys.exit(-1) if pr.is_cross_repo(): print(gh_post_comment(org, project, args.pr_num, "Cross-repo merges are not supported at the moment", dry_run=args.dry_run)) sys.exit(-1) try: pr.merge_into(repo, dry_run=args.dry_run) except Exception as e: gh_post_comment(org, project, args.pr_num, f"Merge failed due to {e}", dry_run=args.dry_run)
def merge_on_green(pr_num: int, repo: GitRepo, dry_run: bool = False) -> None: repo = GitRepo(get_git_repo_dir(), get_git_remote_name()) org, project = repo.gh_owner_and_name() start_time = time.time() last_exception = '' elapsed_time = 0.0 while elapsed_time < 400 * 60: current_time = time.time() elapsed_time = current_time - start_time pr = GitHubPR(org, project, pr_num) try: return pr.merge_into(repo, dry_run=dry_run) except MandatoryChecksMissingError as ex: last_exception = str(ex) print(f"Merged failed due to: {ex}. Retrying in 60 seconds.") time.sleep(60) # Finally report timeout back msg = "Merged timed out after 6 hours. Please contact the pytorch_dev_infra team." msg += f"The last exception was: {last_exception}" if not dry_run: gh_add_labels(org, project, pr_num, ["land-failed"]) raise RuntimeError(msg)
def main() -> None: args = parse_args() repo = GitRepo(get_git_repo_dir(), get_git_remote_name(), debug=True) org, project = repo.gh_owner_and_name() pr = GitHubPR(org, project, args.pr_num) if pr.is_closed(): gh_post_comment(org, project, args.pr_num, f"PR #{args.pr_num} is closed, won't rebase", dry_run=args.dry_run) return if pr.is_ghstack_pr(): gh_post_comment(org, project, args.pr_num, f"PR #{args.pr_num} is a ghstack, which is currently not supported", dry_run=args.dry_run) return try: rebase_onto(pr, repo, dry_run=args.dry_run) except Exception as e: msg = f"Rebase failed due to {e}" run_url = os.getenv("GH_RUN_URL") if run_url is not None: msg += f"\nRaised by {run_url}" gh_post_comment(org, project, args.pr_num, msg, dry_run=args.dry_run)
def rebase_ghstack_onto(pr: GitHubPR, repo: GitRepo, onto_branch: str, dry_run: bool = False) -> None: if subprocess.run([sys.executable, "-m", "ghstack", "--help"], capture_output=True).returncode != 0: subprocess.run([sys.executable, "-m", "pip", "install", "ghstack"]) orig_ref = f"{re.sub(r'/head$', '/orig', pr.head_ref())}" onto_branch = f"refs/remotes/origin/{onto_branch}" repo.fetch(orig_ref, orig_ref) repo._run_git("rebase", onto_branch, orig_ref) # steal the identity of the committer of the commit on the orig branch email = repo._run_git("log", orig_ref, "--pretty=format:%ae", "-1") name = repo._run_git("log", orig_ref, "--pretty=format:%an", "-1") repo._run_git("config", "--global", "user.name", name) repo._run_git("config", "--global", "user.email", email) os.environ["OAUTH_TOKEN"] = os.environ["GITHUB_TOKEN"] with open('.ghstackrc', 'w+') as f: f.write('[ghstack]\n' + "github_url=github.com\n" + "github_username=pytorchmergebot\n" + "remote_name=origin") if dry_run: print("Don't know how to dry-run ghstack") else: ghstack_result = subprocess.run(["ghstack"], capture_output=True) push_result = ghstack_result.stdout.decode("utf-8") print(push_result) if ghstack_result.returncode != 0: raise Exception(f"\n```{push_result}```") # The contents of a successful push result should look like: # Summary of changes (ghstack 0.6.0) # - Updated https://github.com/clee2000/random-testing/pull/2 # - Updated https://github.com/clee2000/random-testing/pull/1 # Facebook employees can import your changes by running # (on a Facebook machine): # ghimport -s https://github.com/clee2000/random-testing/pull/2 # If you want to work on this diff stack on another machine: # ghstack checkout https://github.com/clee2000/random-testing/pull/2 org, project = repo.gh_owner_and_name() for line in push_result.splitlines(): if "Updated" in line: pr_num = int(line.split("/")[-1]) if pr_num != pr.pr_num: gh_post_comment( pr.org, pr.project, pr_num, f"Rebased `{orig_ref}` onto `{onto_branch}` because #{pr.pr_num} was rebased, " "please pull locally before adding more changes (for example, via `ghstack " + f"checkout https://github.com/{org}/{project}/pull/{pr_num}`)", dry_run=dry_run) else: gh_post_comment( pr.org, pr.project, pr_num, f"Successfully rebased `{orig_ref}` onto `{onto_branch}`, please pull locally " + "before adding more changes (for example, via `ghstack " + f"checkout https://github.com/{org}/{project}/pull/{pr.pr_num}`)", dry_run=dry_run) if f"Skipped https://github.com/{org}/{project}/pull/{pr.pr_num}" in push_result: gh_post_comment( pr.org, pr.project, pr.pr_num, f"Tried to rebase and push PR #{pr.pr_num}, but it was already up to date", dry_run=dry_run)
def rebase_ghstack_onto(pr: GitHubPR, repo: GitRepo, dry_run: bool = False, stable: bool = False) -> None: if subprocess.run([sys.executable, "-m", "ghstack", "--help"], capture_output=True).returncode != 0: subprocess.run([sys.executable, "-m", "pip", "install", "ghstack"]) orig_ref = f"{re.sub(r'/head$', '/orig', pr.head_ref())}" onto_branch = "refs/remotes/origin/viable/strict" if stable else pr.default_branch( ) repo.fetch(orig_ref, orig_ref) repo._run_git("rebase", onto_branch, orig_ref) if dry_run: print("Don't know how to dry-run ghstack") else: ghstack_result = subprocess.run(["ghstack"], capture_output=True) push_result = ghstack_result.stdout.decode("utf-8") print(push_result) if ghstack_result.returncode != 0: raise Exception(f"\n```{push_result}```") # The contents of a successful push result should look like: # Summary of changes (ghstack 0.6.0) # - Updated https://github.com/clee2000/random-testing/pull/2 # - Updated https://github.com/clee2000/random-testing/pull/1 # Facebook employees can import your changes by running # (on a Facebook machine): # ghimport -s https://github.com/clee2000/random-testing/pull/2 # If you want to work on this diff stack on another machine: # ghstack checkout https://github.com/clee2000/random-testing/pull/2 org, project = repo.gh_owner_and_name() for line in push_result.splitlines(): if "Updated" in line: pr_num = int(line.split("/")[-1]) if pr_num != pr.pr_num: gh_post_comment( pr.org, pr.project, pr_num, f"Rebased `{orig_ref}` onto `{onto_branch}` because #{pr.pr_num} was rebased, " "please pull locally before adding more changes (for example, via `git checkout " f"{orig_ref} && git pull --rebase`)", dry_run=dry_run) else: gh_post_comment( pr.org, pr.project, pr_num, f"Successfully rebased `{orig_ref}` onto `{onto_branch}`, please pull locally " + f"before adding more changes (for example, via `git checkout {orig_ref} && " + "git pull --rebase`)", dry_run=dry_run) if f"Skipped https://github.com/{org}/{project}/pull/{pr.pr_num}" in push_result: gh_post_comment( pr.org, pr.project, pr.pr_num, f"Tried to rebase and push PR #{pr.pr_num}, but it was already up to date", dry_run=dry_run)