From b7565b81a795750559851ac6d0c91f39ba37e636 Mon Sep 17 00:00:00 2001 From: Peter Evans Date: Fri, 27 Dec 2019 14:40:08 +0900 Subject: [PATCH 01/30] Add v2 alpha --- .gitignore | 1 + action.yml | 14 +- dist/index.js | 42 +- dist/src/common.py | 31 + dist/src/create_or_update_branch.py | 147 +++++ dist/src/create_or_update_pull_request.py | 130 ++++ dist/src/create_pull_request.py | 166 +++++ dist/src/test_common.py | 39 ++ dist/src/test_create_or_update_branch.py | 748 ++++++++++++++++++++++ index.js | 42 +- src/common.py | 31 + src/create_or_update_branch.py | 147 +++++ src/create_or_update_pull_request.py | 130 ++++ src/create_pull_request.py | 166 +++++ src/test_common.py | 39 ++ src/test_create_or_update_branch.py | 748 ++++++++++++++++++++++ 16 files changed, 2564 insertions(+), 57 deletions(-) create mode 100644 dist/src/common.py create mode 100644 dist/src/create_or_update_branch.py create mode 100644 dist/src/create_or_update_pull_request.py create mode 100755 dist/src/create_pull_request.py create mode 100644 dist/src/test_common.py create mode 100644 dist/src/test_create_or_update_branch.py create mode 100644 src/common.py create mode 100644 src/create_or_update_branch.py create mode 100644 src/create_or_update_pull_request.py create mode 100755 src/create_pull_request.py create mode 100644 src/test_common.py create mode 100644 src/test_create_or_update_branch.py diff --git a/.gitignore b/.gitignore index fd4f2b0..4394a72 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ +__pycache__ node_modules .DS_Store diff --git a/action.yml b/action.yml index 599430d..647cef3 100644 --- a/action.yml +++ b/action.yml @@ -6,14 +6,10 @@ inputs: required: true commit-message: description: 'The message to use when committing changes.' - author-name: - description: 'The name of the commit author.' - author-email: - description: 'The email address of the commit author.' - committer-name: - description: 'The name of the committer.' - committer-email: - description: 'The email address of the committer.' + committer: + description: 'The committer name and email address.' + author: + description: 'The author name and email address.' title: description: 'The title of the pull request.' body: @@ -35,7 +31,7 @@ inputs: branch: description: 'The pull request branch name.' base: - description: 'Sets the pull request base branch.' + description: 'The pull request base branch.' branch-suffix: description: 'The branch suffix type.' outputs: diff --git a/dist/index.js b/dist/index.js index 48d46f2..2c373c8 100644 --- a/dist/index.js +++ b/dist/index.js @@ -978,10 +978,8 @@ async function run() { const inputs = { token: core.getInput("token"), commitMessage: core.getInput("commit-message"), - commitAuthorName: core.getInput("author-name"), - commitAuthorEmail: core.getInput("author-email"), - committerName: core.getInput("committer-name"), - committerEmail: core.getInput("committer-email"), + committer: core.getInput("committer"), + author: core.getInput("author"), title: core.getInput("title"), body: core.getInput("body"), labels: core.getInput("labels"), @@ -994,33 +992,29 @@ async function run() { branch: core.getInput("branch"), base: core.getInput("base"), branchSuffix: core.getInput("branch-suffix"), - debugEvent: core.getInput("debug-event") }; core.debug(`Inputs: ${inspect(inputs)}`); // Set environment variables from inputs. if (inputs.token) process.env.GITHUB_TOKEN = inputs.token; - if (inputs.commitMessage) process.env.COMMIT_MESSAGE = inputs.commitMessage; - if (inputs.commitAuthorName) process.env.COMMIT_AUTHOR_NAME = inputs.commitAuthorName; - if (inputs.commitAuthorEmail) process.env.COMMIT_AUTHOR_EMAIL = inputs.commitAuthorEmail; - if (inputs.committerName) process.env.COMMITTER_NAME = inputs.committerName; - if (inputs.committerEmail) process.env.COMMITTER_EMAIL = inputs.committerEmail; - if (inputs.title) process.env.PULL_REQUEST_TITLE = inputs.title; - if (inputs.body) process.env.PULL_REQUEST_BODY = inputs.body; - if (inputs.labels) process.env.PULL_REQUEST_LABELS = inputs.labels; - if (inputs.assignees) process.env.PULL_REQUEST_ASSIGNEES = inputs.assignees; - if (inputs.reviewers) process.env.PULL_REQUEST_REVIEWERS = inputs.reviewers; - if (inputs.teamReviewers) process.env.PULL_REQUEST_TEAM_REVIEWERS = inputs.teamReviewers; - if (inputs.milestone) process.env.PULL_REQUEST_MILESTONE = inputs.milestone; - if (inputs.project) process.env.PROJECT_NAME = inputs.project; - if (inputs.projectColumn) process.env.PROJECT_COLUMN_NAME = inputs.projectColumn; - if (inputs.branch) process.env.PULL_REQUEST_BRANCH = inputs.branch; - if (inputs.base) process.env.PULL_REQUEST_BASE = inputs.base; - if (inputs.branchSuffix) process.env.BRANCH_SUFFIX = inputs.branchSuffix; - if (inputs.debugEvent) process.env.DEBUG_EVENT = inputs.debugEvent; + if (inputs.commitMessage) process.env.CPR_COMMIT_MESSAGE = inputs.commitMessage; + if (inputs.committer) process.env.CPR_COMMITTER = inputs.committer; + if (inputs.author) process.env.CPR_AUTHOR = inputs.author; + if (inputs.title) process.env.CPR_TITLE = inputs.title; + if (inputs.body) process.env.CPR_BODY = inputs.body; + if (inputs.labels) process.env.CPR_LABELS = inputs.labels; + if (inputs.assignees) process.env.CPR_ASSIGNEES = inputs.assignees; + if (inputs.reviewers) process.env.CPR_REVIEWERS = inputs.reviewers; + if (inputs.teamReviewers) process.env.CPR_TEAM_REVIEWERS = inputs.teamReviewers; + if (inputs.milestone) process.env.CPR_MILESTONE = inputs.milestone; + if (inputs.project) process.env.CPR_PROJECT_NAME = inputs.project; + if (inputs.projectColumn) process.env.CPR_PROJECT_COLUMN_NAME = inputs.projectColumn; + if (inputs.branch) process.env.CPR_BRANCH = inputs.branch; + if (inputs.base) process.env.CPR_BASE = inputs.base; + if (inputs.branchSuffix) process.env.CPR_BRANCH_SUFFIX = inputs.branchSuffix; // Execute python script - await exec.exec("python", [`${src}/create-pull-request.py`]); + await exec.exec("python", [`${src}/create_pull_request.py`]); } catch (error) { core.setFailed(error.message); } diff --git a/dist/src/common.py b/dist/src/common.py new file mode 100644 index 0000000..950ac95 --- /dev/null +++ b/dist/src/common.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +import random +import re +import string + + +def get_random_string(length=7, chars=string.ascii_lowercase + string.digits): + return "".join(random.choice(chars) for _ in range(length)) + + +def parse_display_name_email(display_name_email): + # Parse the name and email address from a string in the following format + # Display Name + pattern = re.compile(r"^([^<]+)\s*<([^>]+)>$") + + # Check we have a match + match = pattern.match(display_name_email) + if match is None: + raise ValueError( + f"The format of '{display_name_email}' is not a valid email address with display name" + ) + + # Check that name and email are not just whitespace + name = match.group(1).strip() + email = match.group(2).strip() + if len(name) == 0 or len(email) == 0: + raise ValueError( + f"The format of '{display_name_email}' is not a valid email address with display name" + ) + + return name, email diff --git a/dist/src/create_or_update_branch.py b/dist/src/create_or_update_branch.py new file mode 100644 index 0000000..7683d1a --- /dev/null +++ b/dist/src/create_or_update_branch.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +""" Create or Update Branch """ +import common as cmn +from git import Repo, GitCommandError +import os + + +CHERRYPICK_EMPTY = ( + "The previous cherry-pick is now empty, possibly due to conflict resolution." +) + + +def fetch_successful(repo, repo_url, branch): + try: + repo.git.fetch(repo_url, f"{branch}:refs/remotes/origin/{branch}") + except GitCommandError: + return False + return True + + +def is_ahead(repo, branch_1, branch_2): + # Return true if branch_2 is ahead of branch_1 + return ( + int(repo.git.rev_list("--right-only", "--count", f"{branch_1}...{branch_2}")) + > 0 + ) + + +def is_behind(repo, branch_1, branch_2): + # Return true if branch_2 is behind branch_1 + return ( + int(repo.git.rev_list("--left-only", "--count", f"{branch_1}...{branch_2}")) > 0 + ) + + +def is_even(repo, branch_1, branch_2): + # Return true if branch_2 is even with branch_1 + return not is_ahead(repo, branch_1, branch_2) and not is_behind( + repo, branch_1, branch_2 + ) + + +def has_diff(repo, branch_1, branch_2): + diff = repo.git.diff(f"{branch_1}..{branch_2}") + return len(diff) > 0 + + +def create_or_update_branch(repo, repo_url, commit_message, base, branch): + # Set the default return values + action = "none" + diff = False + + # Get the working base. This may or may not be the actual base. + working_base = repo.git.symbolic_ref("HEAD", "--short") + # If the base is not specified it is assumed to be the working base + if base is None: + base = working_base + + # Save the working base changes to a temporary branch + temp_branch = cmn.get_random_string(length=20) + repo.git.checkout("HEAD", b=temp_branch) + # Commit any uncomitted changes + if repo.is_dirty(untracked_files=True): + print(f"Uncommitted changes found. Adding a commit.") + repo.git.add("-A") + repo.git.commit(m=commit_message) + + # Perform fetch and reset the working base + # Commits made during the workflow will be removed + repo.git.fetch("--force", repo_url, f"{working_base}:{working_base}") + + # If the working base is not the base, rebase the temp branch commits + if working_base != base: + print( + f"Rebasing commits made to branch '{working_base}' on to base branch '{base}'" + ) + # Checkout the actual base + repo.git.fetch("--force", repo_url, f"{base}:{base}") + repo.git.checkout(base) + # Cherrypick commits from the temporary branch starting from the working base + commits = repo.git.rev_list("--reverse", f"{working_base}..{temp_branch}", ".") + for commit in commits.splitlines(): + try: + repo.git.cherry_pick( + "--strategy", + "recursive", + "--strategy-option", + "theirs", + f"{commit}", + ) + except GitCommandError as e: + if CHERRYPICK_EMPTY not in e.stderr: + print("Unexpected error: ", e) + raise + # Reset the temp branch to the working index + repo.git.checkout("-B", temp_branch, "HEAD") + # Reset the base + repo.git.fetch("--force", repo_url, f"{base}:{base}") + + # Try to fetch the pull request branch + if not fetch_successful(repo, repo_url, branch): + # The pull request branch does not exist + print(f"Pull request branch '{branch}' does not exist yet") + # Create the pull request branch + repo.git.checkout("HEAD", b=branch) + # Check if the pull request branch is ahead of the base + diff = is_ahead(repo, base, branch) + if diff: + action = "created" + print(f"Created branch '{branch}'") + else: + print( + f"Branch '{branch}' is not ahead of base '{base}' and will not be created" + ) + else: + # The pull request branch exists + print( + f"Pull request branch '{branch}' already exists as remote branch 'origin/{branch}'" + ) + # Checkout the pull request branch + repo.git.checkout(branch) + + if has_diff(repo, branch, temp_branch): + # If the branch differs from the recreated temp version then the branch is reset + # For changes on base this action is similar to a rebase of the pull request branch + print(f"Resetting '{branch}'") + repo.git.checkout("-B", branch, temp_branch) + # repo.git.switch("-C", branch, temp_branch) + + # Check if the pull request branch has been updated + # If the branch was reset or updated it will be ahead + # It may be behind if a reset now results in no diff with the base + if not is_even(repo, f"origin/{branch}", branch): + action = "updated" + print(f"Updated branch '{branch}'") + else: + print( + f"Branch '{branch}' is even with its remote and will not be updated" + ) + + # Check if the pull request branch is ahead of the base + diff = is_ahead(repo, base, branch) + + # Delete the temporary branch + repo.git.branch("--delete", "--force", temp_branch) + + return {"action": action, "diff": diff, "base": base} diff --git a/dist/src/create_or_update_pull_request.py b/dist/src/create_or_update_pull_request.py new file mode 100644 index 0000000..52b3101 --- /dev/null +++ b/dist/src/create_or_update_pull_request.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +""" Create or Update Pull Request """ +from github import Github, GithubException +import os + + +def cs_string_to_list(str): + # Split the comma separated string into a list + l = [i.strip() for i in str.split(",")] + # Remove empty strings + return list(filter(None, l)) + + +def create_project_card(github_repo, project_name, project_column_name, pull_request): + # Locate the project by name + project = None + for project_item in github_repo.get_projects("all"): + if project_item.name == project_name: + project = project_item + break + + if not project: + print("::warning::Project not found. Unable to create project card.") + return + + # Locate the column by name + column = None + for column_item in project.get_columns(): + if column_item.name == project_column_name: + column = column_item + break + + if not column: + print("::warning::Project column not found. Unable to create project card.") + return + + # Create a project card for the pull request + column.create_card(content_id=pull_request.id, content_type="PullRequest") + print( + "Added pull request #%d to project '%s' under column '%s'" + % (pull_request.number, project.name, column.name) + ) + + +def create_or_update_pull_request( + github_token, + github_repository, + branch, + base, + title, + body, + labels, + assignees, + milestone, + reviewers, + team_reviewers, + project_name, + project_column_name, +): + # Create the pull request + github_repo = Github(github_token).get_repo(github_repository) + try: + pull_request = github_repo.create_pull( + title=title, body=body, base=base, head=branch + ) + print( + "Created pull request #%d (%s => %s)" % (pull_request.number, branch, base) + ) + except GithubException as e: + if e.status == 422: + # Format the branch name + head_branch = "%s:%s" % (github_repository.split("/")[0], branch) + # Get the pull request + pull_request = github_repo.get_pulls( + state="open", base=base, head=head_branch + )[0] + print( + "Updated pull request #%d (%s => %s)" + % (pull_request.number, branch, base) + ) + else: + print(str(e)) + raise + + # Set the output variables + os.system("echo ::set-env name=PULL_REQUEST_NUMBER::%d" % pull_request.number) + os.system("echo ::set-output name=pr_number::%d" % pull_request.number) + + # Set labels, assignees and milestone + if labels is not None: + print("Applying labels '%s'" % labels) + pull_request.as_issue().edit(labels=cs_string_to_list(labels)) + if assignees is not None: + print("Applying assignees '%s'" % assignees) + pull_request.as_issue().edit(assignees=cs_string_to_list(assignees)) + if milestone is not None: + print("Applying milestone '%s'" % milestone) + milestone = github_repo.get_milestone(int(milestone)) + pull_request.as_issue().edit(milestone=milestone) + + # Set pull request reviewers + if reviewers is not None: + print("Requesting reviewers '%s'" % reviewers) + try: + pull_request.create_review_request(reviewers=cs_string_to_list(reviewers)) + except GithubException as e: + # Likely caused by "Review cannot be requested from pull request + # author." + if e.status == 422: + print("Requesting reviewers failed - %s" % e.data["message"]) + + # Set pull request team reviewers + if team_reviewers is not None: + print("Requesting team reviewers '%s'" % team_reviewers) + pull_request.create_review_request( + team_reviewers=cs_string_to_list(team_reviewers) + ) + + # Create a project card for the pull request + if project_name is not None and project_column_name is not None: + try: + create_project_card( + github_repo, project_name, project_column_name, pull_request + ) + except GithubException as e: + # Likely caused by "Project already has the associated issue." + if e.status == 422: + print( + "Create project card failed - %s" % e.data["errors"][0]["message"] + ) diff --git a/dist/src/create_pull_request.py b/dist/src/create_pull_request.py new file mode 100755 index 0000000..31e602a --- /dev/null +++ b/dist/src/create_pull_request.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +""" Create Pull Request """ +import common as cmn +import create_or_update_branch as coub +import create_or_update_pull_request as coupr +from git import Repo +import json +import os +import sys +import time + + +# Default the committer and author to the GitHub Actions bot +DEFAULT_COMMITTER = "GitHub " +DEFAULT_AUTHOR = ( + "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>" +) + + +def set_committer_author(repo, committer, author): + # When the user intends for the committer and author to be the same, + # ideally, just the committer should be supplied. When just the author + # is supplied, the same user intention is assumed. + if committer is None and author is not None: + print("Supplied author will also be used as the committer.") + committer = author + + # TODO Get committer and author from git config + # If just a committer exists, only set committer + # If just author exists also use for the committer + + # Set defaults if no committer/author has been supplied + if committer is None and author is None: + committer = DEFAULT_COMMITTER + author = DEFAULT_AUTHOR + + # Set git environment. This will not persist after the action completes. + committer_name, committer_email = cmn.parse_display_name_email(committer) + print(f"Configuring git committer as '{committer_name} <{committer_email}>'") + if author is not None: + author_name, author_email = cmn.parse_display_name_email(author) + print(f"Configuring git author as '{author_name} <{author_email}>'") + repo.git.update_environment( + GIT_COMMITTER_NAME=committer_name, + GIT_COMMITTER_EMAIL=committer_email, + GIT_AUTHOR_NAME=author_name, + GIT_AUTHOR_EMAIL=author_email, + ) + else: + repo.git.update_environment( + GIT_COMMITTER_NAME=committer_name, GIT_COMMITTER_EMAIL=committer_email, + ) + + +# Get required environment variables +github_token = os.environ["GITHUB_TOKEN"] +github_repository = os.environ["GITHUB_REPOSITORY"] +# Get environment variables with defaults +branch = os.getenv("CPR_BRANCH", "create-pull-request/patch") +commit_message = os.getenv( + "CPR_COMMIT_MESSAGE", "Changes by create-pull-request action" +) +# Get environment variables with a default of 'None' +committer = os.environ.get("CPR_COMMITTER") +author = os.environ.get("CPR_AUTHOR") +base = os.environ.get("CPR_BASE") + +# Set the repo to the working directory +repo = Repo(os.getcwd()) + +# Determine if the checked out ref is a valid base for a pull request +# The action needs the checked out HEAD ref to be a branch +# This check will fail in the following cases: +# - HEAD is detached +# - HEAD is a merge commit (pull_request events) +# - HEAD is a tag +try: + working_base = repo.git.symbolic_ref("HEAD", "--short") +except: + print(f"::debug::{working_base}") + print( + f"::error::The checked out ref is not a valid base for a pull request. " + + "Unable to continue. Exiting." + ) + sys.exit(1) + +# Exit if the working base is a PR branch created by this action. +# This may occur when using a PAT instead of GITHUB_TOKEN because +# a PAT allows workflow actions to trigger further events. +if working_base.startswith(branch): + print( + f"::error::Working base branch '{working_base}' was created by this action. " + + "Unable to continue. Exiting." + ) + sys.exit(1) + +# Fetch an optional environment variable to determine the branch suffix +branch_suffix = os.environ.get("CPR_BRANCH_SUFFIX") +if branch_suffix is not None: + if branch_suffix == "short-commit-hash": + # Suffix with the short SHA1 hash + branch = "{}-{}".format(branch, repo.git.rev_parse("--short", "HEAD")) + elif branch_suffix == "timestamp": + # Suffix with the current timestamp + branch = "{}-{}".format(branch, int(time.time())) + elif branch_suffix == "random": + # Suffix with a 7 character random string + branch = "{}-{}".format(branch, cmn.get_random_string()) + else: + print( + f"::error::Branch suffix '{branch_suffix}' is not a valid value. " + + "Unable to continue. Exiting." + ) + sys.exit(1) + +# Output head branch +print(f"Pull request branch to create or update set to '{branch}'") + +# Set the committer and author +try: + set_committer_author(repo, committer, author) +except ValueError as e: + print(f"::error::{e} " + "Unable to continue. Exiting.") + sys.exit(1) + +# Set the repository URL +repo_url = f"https://x-access-token:{github_token}@github.com/{github_repository}" + +# Create or update the pull request branch +result = coub.create_or_update_branch(repo, repo_url, commit_message, base, branch) + +if result["action"] in ["created", "updated"]: + # The branch was created or updated + print(f"Pushing pull request branch to 'origin/{branch}'") + repo.git.push("--force", repo_url, f"HEAD:refs/heads/{branch}") + + # Set the base. It would have been 'None' if not specified as an input + base = result["base"] + + # TODO Figure out what to do when there is no diff with the base anymore + # if not result["diff"]: + + # Fetch optional environment variables with default values + title = os.getenv("CPR_TITLE", "Auto-generated by create-pull-request action") + body = os.getenv( + "CPR_BODY", + "Auto-generated pull request by " + "[create-pull-request](https://github.com/peter-evans/create-pull-request) GitHub Action", + ) + + # Create or update the pull request + coupr.create_or_update_pull_request( + github_token, + github_repository, + branch, + base, + title, + body, + os.environ.get("CPR_LABELS"), + os.environ.get("CPR_ASSIGNEES"), + os.environ.get("CPR_MILESTONE"), + os.environ.get("CPR_REVIEWERS"), + os.environ.get("CPR_TEAM_REVIEWERS"), + os.environ.get("CPR_PROJECT_NAME"), + os.environ.get("CPR_PROJECT_COLUMN_NAME"), + ) diff --git a/dist/src/test_common.py b/dist/src/test_common.py new file mode 100644 index 0000000..2d7dd2b --- /dev/null +++ b/dist/src/test_common.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +""" Test Common """ +import common as cmn +import pytest + + +def test_get_random_string(): + assert len(cmn.get_random_string()) == 7 + assert len(cmn.get_random_string(length=20)) == 20 + + +def test_parse_display_name_email_success(): + name, email = cmn.parse_display_name_email("abc def ") + assert name == "abc def" + assert email == "abc@def.com" + + name, email = cmn.parse_display_name_email( + "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>" + ) + assert name == "github-actions[bot]" + assert email == "41898282+github-actions[bot]@users.noreply.github.com" + + +def test_parse_display_name_email_failure(): + display_name_email = "abc@def.com" + with pytest.raises(ValueError) as e_info: + cmn.parse_display_name_email(display_name_email) + assert ( + e_info.value.args[0] + == f"The format of '{display_name_email}' is not a valid email address with display name" + ) + + display_name_email = " < >" + with pytest.raises(ValueError) as e_info: + cmn.parse_display_name_email(display_name_email) + assert ( + e_info.value.args[0] + == f"The format of '{display_name_email}' is not a valid email address with display name" + ) diff --git a/dist/src/test_create_or_update_branch.py b/dist/src/test_create_or_update_branch.py new file mode 100644 index 0000000..c6ff95b --- /dev/null +++ b/dist/src/test_create_or_update_branch.py @@ -0,0 +1,748 @@ +#!/usr/bin/env python3 +""" Test Create or Update Branch """ +import create_or_update_branch as coub +from git import Repo +import os +import pytest +import sys +import time + + +author_name = "github-actions[bot]" +author_email = "41898282+github-actions[bot]@users.noreply.github.com" +committer_name = "GitHub" +committer_email = "noreply@github.com" + +# Set git environment +repo = Repo(os.getcwd()) +repo.git.update_environment( + GIT_AUTHOR_NAME=author_name, + GIT_AUTHOR_EMAIL=author_email, + GIT_COMMITTER_NAME=committer_name, + GIT_COMMITTER_EMAIL=committer_email, +) + +REPO_URL = repo.git.config("--get", "remote.origin.url") + +TRACKED_FILE = "tracked-file.txt" +UNTRACKED_FILE = "untracked-file.txt" + +DEFAULT_BRANCH = "tests/master" +NOT_BASE_BRANCH = "tests/branch-that-is-not-the-base" +NOT_EXIST_BRANCH = "tests/branch-that-does-not-exist" + +COMMIT_MESSAGE = "Changes by create-pull-request action" +BRANCH = "tests/create-pull-request/patch" +BASE = DEFAULT_BRANCH + + +def create_tracked_change(content=None): + if content is None: + content = str(time.time()) + # Create a tracked file change + with open(TRACKED_FILE, "w") as f: + f.write(content) + return content + + +def create_untracked_change(content=None): + if content is None: + content = str(time.time()) + # Create an untracked file change + with open(UNTRACKED_FILE, "w") as f: + f.write(content) + return content + + +def get_tracked_content(): + # Read the content of the tracked file + with open(TRACKED_FILE, "r") as f: + return f.read() + + +def get_untracked_content(): + # Read the content of the untracked file + with open(UNTRACKED_FILE, "r") as f: + return f.read() + + +def create_changes(tracked_content=None, untracked_content=None): + tracked_content = create_tracked_change(tracked_content) + untracked_content = create_untracked_change(untracked_content) + return tracked_content, untracked_content + + +def create_commits(number=2, final_tracked_content=None, final_untracked_content=None): + for i in range(number): + commit_number = i + 1 + if commit_number == number: + tracked_content, untracked_content = create_changes( + final_tracked_content, final_untracked_content + ) + else: + tracked_content, untracked_content = create_changes() + repo.git.add("-A") + repo.git.commit(m=f"Commit {commit_number}") + return tracked_content, untracked_content + + +@pytest.fixture(scope="module", autouse=True) +def before_after_all(): + print("Before all tests") + # Check there are no local changes that might be + # destroyed by running these tests + assert not repo.is_dirty(untracked_files=True) + + # Create a new default branch for the test run + repo.git.checkout("master") + repo.git.checkout("HEAD", b=NOT_BASE_BRANCH) + create_tracked_change() + repo.git.add("-A") + repo.git.commit(m="This commit should not appear in pr branches") + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{NOT_BASE_BRANCH}") + # Create a new default branch for the test run + repo.git.checkout("master") + repo.git.checkout("HEAD", b=DEFAULT_BRANCH) + create_tracked_change() + repo.git.add("-A") + repo.git.commit(m="Add file to be a tracked file for tests") + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{DEFAULT_BRANCH}") + + yield + + print("After all tests") + repo.git.checkout("master") + # Delete the "not base branch" created for the test run + repo.git.branch("--delete", "--force", NOT_BASE_BRANCH) + repo.git.push("--delete", "--force", REPO_URL, f"refs/heads/{NOT_BASE_BRANCH}") + # Delete the default branch created for the test run + repo.git.branch("--delete", "--force", DEFAULT_BRANCH) + repo.git.push("--delete", "--force", REPO_URL, f"refs/heads/{DEFAULT_BRANCH}") + + +def before_test(): + print("Before test") + # Checkout the default branch + repo.git.checkout(DEFAULT_BRANCH) + + +def after_test(delete_remote=True): + print("After test") + # Output git log + print(repo.git.log("-5", pretty="oneline")) + # Delete the pull request branch if it exists + repo.git.checkout(DEFAULT_BRANCH) + print(f"Deleting {BRANCH}") + for branch in repo.branches: + if branch.name == BRANCH: + repo.git.branch("--delete", "--force", BRANCH) + break + if delete_remote: + print(f"Deleting origin/{BRANCH}") + for ref in repo.remotes.origin.refs: + if ref.name == f"origin/{BRANCH}": + repo.git.push("--delete", "--force", REPO_URL, f"refs/heads/{BRANCH}") + repo.remotes.origin.fetch("--prune") + break + + +@pytest.fixture(autouse=True) +def before_after_tests(): + before_test() + yield + after_test() + + +# Tests if a branch exists and can be fetched +def coub_fetch_successful(): + assert coub.fetch_successful(repo, REPO_URL, NOT_BASE_BRANCH) + assert not coub.fetch_successful(repo, REPO_URL, NOT_EXIST_BRANCH) + + +# Tests no changes resulting in no new branch being created +def coub_no_changes_on_create(): + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH) + assert result["action"] == "none" + + +# Tests create and update with a tracked file change +def coub_tracked_changes(): + # Create a tracked file change + tracked_content = create_tracked_change() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH) + assert result["action"] == "created" + assert get_tracked_content() == tracked_content + + # Push pull request branch to remote + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}") + repo.remotes.origin.fetch() + + after_test(delete_remote=False) + before_test() + + # Create a tracked file change + tracked_content = create_tracked_change() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH) + assert result["action"] == "updated" + assert result["diff"] + assert get_tracked_content() == tracked_content + + +# Tests create and update with an untracked file change +def coub_untracked_changes(): + # Create an untracked file change + untracked_content = create_untracked_change() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH) + assert result["action"] == "created" + assert get_untracked_content() == untracked_content + + # Push pull request branch to remote + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}") + repo.remotes.origin.fetch() + + after_test(delete_remote=False) + before_test() + + # Create an untracked file change + untracked_content = create_untracked_change() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH) + assert result["action"] == "updated" + assert result["diff"] + assert get_untracked_content() == untracked_content + + +# Tests create and update with identical changes +# The pull request branch will not be updated +def coub_identical_changes(): + # Create tracked and untracked file changes + tracked_content, untracked_content = create_changes() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH) + assert result["action"] == "created" + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + # Push pull request branch to remote + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}") + repo.remotes.origin.fetch() + + after_test(delete_remote=False) + before_test() + + # Create identical tracked and untracked file changes + create_changes(tracked_content, untracked_content) + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH) + assert result["action"] == "none" + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + +# Tests create and update with commits on the base inbetween +def coub_commits_on_base(): + # Create tracked and untracked file changes + tracked_content, untracked_content = create_changes() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH) + assert result["action"] == "created" + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + # Push pull request branch to remote + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}") + repo.remotes.origin.fetch() + + after_test(delete_remote=False) + before_test() + + # Create commits on the base + create_commits() + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{DEFAULT_BRANCH}") + repo.remotes.origin.fetch() + + # Create tracked and untracked file changes + tracked_content, untracked_content = create_changes() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH) + assert result["action"] == "updated" + assert result["diff"] + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + +# Tests create and then an update with no changes +# This effectively reverts the branch back to match the base and results in no diff +def coub_changes_no_diff(): + # Save the default branch tracked content + default_tracked_content = get_tracked_content() + + # Create tracked and untracked file changes + tracked_content, untracked_content = create_changes() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH) + assert result["action"] == "created" + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + # Push pull request branch to remote + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}") + repo.remotes.origin.fetch() + + after_test(delete_remote=False) + before_test() + + # Running with no update effectively reverts the branch back to match the base + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH) + assert result["action"] == "updated" + assert result["diff"] == False + assert get_tracked_content() == default_tracked_content + + +# Tests create and update with commits on the base inbetween +# The changes on base effectively revert the branch back to match the base and results in no diff +def coub_commits_on_base_no_diff(): + # Create tracked and untracked file changes + tracked_content, untracked_content = create_changes() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH) + assert result["action"] == "created" + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + # Push pull request branch to remote + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}") + repo.remotes.origin.fetch() + + after_test(delete_remote=False) + before_test() + + # Create commits on the base + tracked_content, untracked_content = create_commits() + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{DEFAULT_BRANCH}") + repo.remotes.origin.fetch() + + # Create the same tracked and untracked file changes that were made to the base + create_changes(tracked_content, untracked_content) + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH) + assert result["action"] == "updated" + assert result["diff"] == False + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + +# Tests create and update with commits on the working base (during the workflow) +def coub_commits_on_working_base(): + # Create commits on the working base + tracked_content, untracked_content = create_commits() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH) + assert result["action"] == "created" + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + # Push pull request branch to remote + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}") + repo.remotes.origin.fetch() + + after_test(delete_remote=False) + before_test() + + # Create commits on the working base + tracked_content, untracked_content = create_commits() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH) + assert result["action"] == "updated" + assert result["diff"] + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + +# Tests create and update with changes and commits on the working base (during the workflow) +def coub_changes_and_commits_on_working_base(): + # Create commits on the working base + create_commits() + # Create tracked and untracked file changes + tracked_content, untracked_content = create_changes() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH) + assert result["action"] == "created" + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + # Push pull request branch to remote + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}") + repo.remotes.origin.fetch() + + after_test(delete_remote=False) + before_test() + + # Create commits on the working base + create_commits() + # Create tracked and untracked file changes + tracked_content, untracked_content = create_changes() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH) + assert result["action"] == "updated" + assert result["diff"] + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + +# Tests create and update with changes and commits on the working base (during the workflow) +# with commits on the base inbetween +def coub_changes_and_commits_on_base_and_working_base(): + # Create commits on the working base + create_commits() + # Create tracked and untracked file changes + tracked_content, untracked_content = create_changes() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH) + assert result["action"] == "created" + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + # Push pull request branch to remote + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}") + repo.remotes.origin.fetch() + + after_test(delete_remote=False) + before_test() + + # Create commits on the base + create_commits() + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{DEFAULT_BRANCH}") + repo.remotes.origin.fetch() + + # Create commits on the working base + create_commits() + # Create tracked and untracked file changes + tracked_content, untracked_content = create_changes() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH) + assert result["action"] == "updated" + assert result["diff"] + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + +# Working Base is Not Base (WBNB) +# Tests no changes resulting in no new branch being created +def coub_wbnb_no_changes_on_create(): + # Set the working base to a branch that is not the pull request base + repo.git.checkout(NOT_BASE_BRANCH) + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH) + assert result["action"] == "none" + + +# Working Base is Not Base (WBNB) +# Tests create and update with a tracked file change +def coub_wbnb_tracked_changes(): + # Set the working base to a branch that is not the pull request base + repo.git.checkout(NOT_BASE_BRANCH) + # Create a tracked file change + tracked_content = create_tracked_change() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH) + assert result["action"] == "created" + assert get_tracked_content() == tracked_content + + # Push pull request branch to remote + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}") + repo.remotes.origin.fetch() + + after_test(delete_remote=False) + before_test() + + # Set the working base to a branch that is not the pull request base + repo.git.checkout(NOT_BASE_BRANCH) + # Create a tracked file change + tracked_content = create_tracked_change() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH) + assert result["action"] == "updated" + assert result["diff"] + assert get_tracked_content() == tracked_content + + +# Working Base is Not Base (WBNB) +# Tests create and update with an untracked file change +def coub_wbnb_untracked_changes(): + # Set the working base to a branch that is not the pull request base + repo.git.checkout(NOT_BASE_BRANCH) + # Create an untracked file change + untracked_content = create_untracked_change() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH) + assert result["action"] == "created" + assert get_untracked_content() == untracked_content + + # Push pull request branch to remote + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}") + repo.remotes.origin.fetch() + + after_test(delete_remote=False) + before_test() + + # Set the working base to a branch that is not the pull request base + repo.git.checkout(NOT_BASE_BRANCH) + # Create an untracked file change + untracked_content = create_untracked_change() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH) + assert result["action"] == "updated" + assert result["diff"] + assert get_untracked_content() == untracked_content + + +# Working Base is Not Base (WBNB) +# Tests create and update with identical changes +# The pull request branch will not be updated +def coub_wbnb_identical_changes(): + # Set the working base to a branch that is not the pull request base + repo.git.checkout(NOT_BASE_BRANCH) + # Create tracked and untracked file changes + tracked_content, untracked_content = create_changes() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH) + assert result["action"] == "created" + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + # Push pull request branch to remote + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}") + repo.remotes.origin.fetch() + + after_test(delete_remote=False) + before_test() + + # Set the working base to a branch that is not the pull request base + repo.git.checkout(NOT_BASE_BRANCH) + # Create identical tracked and untracked file changes + create_changes(tracked_content, untracked_content) + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH) + assert result["action"] == "none" + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + +# Working Base is Not Base (WBNB) +# Tests create and update with commits on the base inbetween +def coub_wbnb_commits_on_base(): + # Set the working base to a branch that is not the pull request base + repo.git.checkout(NOT_BASE_BRANCH) + # Create tracked and untracked file changes + tracked_content, untracked_content = create_changes() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH) + assert result["action"] == "created" + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + # Push pull request branch to remote + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}") + repo.remotes.origin.fetch() + + after_test(delete_remote=False) + before_test() + + # Create commits on the base + create_commits() + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{DEFAULT_BRANCH}") + repo.remotes.origin.fetch() + + # Set the working base to a branch that is not the pull request base + repo.git.checkout(NOT_BASE_BRANCH) + # Create tracked and untracked file changes + tracked_content, untracked_content = create_changes() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH) + assert result["action"] == "updated" + assert result["diff"] + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + +# Working Base is Not Base (WBNB) +# Tests create and then an update with no changes +# This effectively reverts the branch back to match the base and results in no diff +def coub_wbnb_changes_no_diff(): + # Save the default branch tracked content + default_tracked_content = get_tracked_content() + # Set the working base to a branch that is not the pull request base + repo.git.checkout(NOT_BASE_BRANCH) + # Create tracked and untracked file changes + tracked_content, untracked_content = create_changes() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH) + assert result["action"] == "created" + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + # Push pull request branch to remote + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}") + repo.remotes.origin.fetch() + + after_test(delete_remote=False) + before_test() + + # Set the working base to a branch that is not the pull request base + repo.git.checkout(NOT_BASE_BRANCH) + # Running with no update effectively reverts the branch back to match the base + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH) + assert result["action"] == "updated" + assert result["diff"] == False + assert get_tracked_content() == default_tracked_content + + +# Working Base is Not Base (WBNB) +# Tests create and update with commits on the base inbetween +# The changes on base effectively revert the branch back to match the base and results in no diff +# This scenario will cause cherrypick to fail due to an empty commit. +# The commit is empty because the changes now exist on the base. +def coub_wbnb_commits_on_base_no_diff(): + # Set the working base to a branch that is not the pull request base + repo.git.checkout(NOT_BASE_BRANCH) + # Create tracked and untracked file changes + tracked_content, untracked_content = create_changes() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH) + assert result["action"] == "created" + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + # Push pull request branch to remote + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}") + repo.remotes.origin.fetch() + + after_test(delete_remote=False) + before_test() + + # Create commits on the base + tracked_content, untracked_content = create_commits() + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{DEFAULT_BRANCH}") + repo.remotes.origin.fetch() + + # Set the working base to a branch that is not the pull request base + repo.git.checkout(NOT_BASE_BRANCH) + # Create the same tracked and untracked file changes that were made to the base + create_changes(tracked_content, untracked_content) + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH) + assert result["action"] == "updated" + assert result["diff"] == False + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + +# Working Base is Not Base (WBNB) +# Tests create and update with commits on the working base (during the workflow) +def coub_wbnb_commits_on_working_base(): + # Set the working base to a branch that is not the pull request base + repo.git.checkout(NOT_BASE_BRANCH) + # Create commits on the working base + tracked_content, untracked_content = create_commits() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH) + assert result["action"] == "created" + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + # Push pull request branch to remote + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}") + repo.remotes.origin.fetch() + + after_test(delete_remote=False) + before_test() + + # Set the working base to a branch that is not the pull request base + repo.git.checkout(NOT_BASE_BRANCH) + # Create commits on the working base + tracked_content, untracked_content = create_commits() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH) + assert result["action"] == "updated" + assert result["diff"] + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + +# Working Base is Not Base (WBNB) +# Tests create and update with changes and commits on the working base (during the workflow) +def coub_wbnb_changes_and_commits_on_working_base(): + # Set the working base to a branch that is not the pull request base + repo.git.checkout(NOT_BASE_BRANCH) + # Create commits on the working base + create_commits() + # Create tracked and untracked file changes + tracked_content, untracked_content = create_changes() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH) + assert result["action"] == "created" + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + # Push pull request branch to remote + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}") + repo.remotes.origin.fetch() + + after_test(delete_remote=False) + before_test() + + # Set the working base to a branch that is not the pull request base + repo.git.checkout(NOT_BASE_BRANCH) + # Create commits on the working base + create_commits() + # Create tracked and untracked file changes + tracked_content, untracked_content = create_changes() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH) + assert result["action"] == "updated" + assert result["diff"] + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + +# Working Base is Not Base (WBNB) +# Tests create and update with changes and commits on the working base (during the workflow) +# with commits on the base inbetween +def coub_wbnb_changes_and_commits_on_base_and_working_base(): + # Set the working base to a branch that is not the pull request base + repo.git.checkout(NOT_BASE_BRANCH) + # Create commits on the working base + create_commits() + # Create tracked and untracked file changes + tracked_content, untracked_content = create_changes() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH) + assert result["action"] == "created" + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + # Push pull request branch to remote + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}") + repo.remotes.origin.fetch() + + after_test(delete_remote=False) + before_test() + + # Create commits on the base + create_commits() + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{DEFAULT_BRANCH}") + repo.remotes.origin.fetch() + + # Set the working base to a branch that is not the pull request base + repo.git.checkout(NOT_BASE_BRANCH) + # Create commits on the working base + create_commits() + # Create tracked and untracked file changes + tracked_content, untracked_content = create_changes() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH) + assert result["action"] == "updated" + assert result["diff"] + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + +# pytest -v -s ~/git/create-pull-request/src + +# test_coub_fetch_successful = coub_fetch_successful + +# test_coub_no_changes_on_create = coub_no_changes_on_create +# test_coub_tracked_changes = coub_tracked_changes +# test_coub_untracked_changes = coub_untracked_changes +# test_coub_identical_changes = coub_identical_changes +# test_coub_commits_on_base = coub_commits_on_base + +# test_coub_changes_no_diff = coub_changes_no_diff +# test_coub_commits_on_base_no_diff = coub_commits_on_base_no_diff + +# test_coub_commits_on_working_base = coub_commits_on_working_base +# test_coub_changes_and_commits_on_working_base = coub_changes_and_commits_on_working_base +# test_coub_changes_and_commits_on_base_and_working_base = coub_changes_and_commits_on_base_and_working_base + +# # WBNB +# test_coub_wbnb_no_changes_on_create = coub_wbnb_no_changes_on_create +# test_coub_wbnb_tracked_changes = coub_wbnb_tracked_changes +# test_coub_wbnb_untracked_changes = coub_wbnb_untracked_changes +# test_coub_wbnb_identical_changes = coub_wbnb_identical_changes +# test_coub_wbnb_commits_on_base = coub_wbnb_commits_on_base + +# test_coub_wbnb_changes_no_diff = coub_wbnb_changes_no_diff +# test_coub_wbnb_commits_on_base_no_diff = coub_wbnb_commits_on_base_no_diff + +# test_coub_wbnb_commits_on_working_base = coub_wbnb_commits_on_working_base +# test_coub_wbnb_changes_and_commits_on_working_base = coub_wbnb_changes_and_commits_on_working_base +# test_coub_wbnb_changes_and_commits_on_base_and_working_base = coub_wbnb_changes_and_commits_on_base_and_working_base diff --git a/index.js b/index.js index e95f469..d27b62b 100644 --- a/index.js +++ b/index.js @@ -23,10 +23,8 @@ async function run() { const inputs = { token: core.getInput("token"), commitMessage: core.getInput("commit-message"), - commitAuthorName: core.getInput("author-name"), - commitAuthorEmail: core.getInput("author-email"), - committerName: core.getInput("committer-name"), - committerEmail: core.getInput("committer-email"), + committer: core.getInput("committer"), + author: core.getInput("author"), title: core.getInput("title"), body: core.getInput("body"), labels: core.getInput("labels"), @@ -39,33 +37,29 @@ async function run() { branch: core.getInput("branch"), base: core.getInput("base"), branchSuffix: core.getInput("branch-suffix"), - debugEvent: core.getInput("debug-event") }; core.debug(`Inputs: ${inspect(inputs)}`); // Set environment variables from inputs. if (inputs.token) process.env.GITHUB_TOKEN = inputs.token; - if (inputs.commitMessage) process.env.COMMIT_MESSAGE = inputs.commitMessage; - if (inputs.commitAuthorName) process.env.COMMIT_AUTHOR_NAME = inputs.commitAuthorName; - if (inputs.commitAuthorEmail) process.env.COMMIT_AUTHOR_EMAIL = inputs.commitAuthorEmail; - if (inputs.committerName) process.env.COMMITTER_NAME = inputs.committerName; - if (inputs.committerEmail) process.env.COMMITTER_EMAIL = inputs.committerEmail; - if (inputs.title) process.env.PULL_REQUEST_TITLE = inputs.title; - if (inputs.body) process.env.PULL_REQUEST_BODY = inputs.body; - if (inputs.labels) process.env.PULL_REQUEST_LABELS = inputs.labels; - if (inputs.assignees) process.env.PULL_REQUEST_ASSIGNEES = inputs.assignees; - if (inputs.reviewers) process.env.PULL_REQUEST_REVIEWERS = inputs.reviewers; - if (inputs.teamReviewers) process.env.PULL_REQUEST_TEAM_REVIEWERS = inputs.teamReviewers; - if (inputs.milestone) process.env.PULL_REQUEST_MILESTONE = inputs.milestone; - if (inputs.project) process.env.PROJECT_NAME = inputs.project; - if (inputs.projectColumn) process.env.PROJECT_COLUMN_NAME = inputs.projectColumn; - if (inputs.branch) process.env.PULL_REQUEST_BRANCH = inputs.branch; - if (inputs.base) process.env.PULL_REQUEST_BASE = inputs.base; - if (inputs.branchSuffix) process.env.BRANCH_SUFFIX = inputs.branchSuffix; - if (inputs.debugEvent) process.env.DEBUG_EVENT = inputs.debugEvent; + if (inputs.commitMessage) process.env.CPR_COMMIT_MESSAGE = inputs.commitMessage; + if (inputs.committer) process.env.CPR_COMMITTER = inputs.committer; + if (inputs.author) process.env.CPR_AUTHOR = inputs.author; + if (inputs.title) process.env.CPR_TITLE = inputs.title; + if (inputs.body) process.env.CPR_BODY = inputs.body; + if (inputs.labels) process.env.CPR_LABELS = inputs.labels; + if (inputs.assignees) process.env.CPR_ASSIGNEES = inputs.assignees; + if (inputs.reviewers) process.env.CPR_REVIEWERS = inputs.reviewers; + if (inputs.teamReviewers) process.env.CPR_TEAM_REVIEWERS = inputs.teamReviewers; + if (inputs.milestone) process.env.CPR_MILESTONE = inputs.milestone; + if (inputs.project) process.env.CPR_PROJECT_NAME = inputs.project; + if (inputs.projectColumn) process.env.CPR_PROJECT_COLUMN_NAME = inputs.projectColumn; + if (inputs.branch) process.env.CPR_BRANCH = inputs.branch; + if (inputs.base) process.env.CPR_BASE = inputs.base; + if (inputs.branchSuffix) process.env.CPR_BRANCH_SUFFIX = inputs.branchSuffix; // Execute python script - await exec.exec("python", [`${src}/create-pull-request.py`]); + await exec.exec("python", [`${src}/create_pull_request.py`]); } catch (error) { core.setFailed(error.message); } diff --git a/src/common.py b/src/common.py new file mode 100644 index 0000000..950ac95 --- /dev/null +++ b/src/common.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +import random +import re +import string + + +def get_random_string(length=7, chars=string.ascii_lowercase + string.digits): + return "".join(random.choice(chars) for _ in range(length)) + + +def parse_display_name_email(display_name_email): + # Parse the name and email address from a string in the following format + # Display Name + pattern = re.compile(r"^([^<]+)\s*<([^>]+)>$") + + # Check we have a match + match = pattern.match(display_name_email) + if match is None: + raise ValueError( + f"The format of '{display_name_email}' is not a valid email address with display name" + ) + + # Check that name and email are not just whitespace + name = match.group(1).strip() + email = match.group(2).strip() + if len(name) == 0 or len(email) == 0: + raise ValueError( + f"The format of '{display_name_email}' is not a valid email address with display name" + ) + + return name, email diff --git a/src/create_or_update_branch.py b/src/create_or_update_branch.py new file mode 100644 index 0000000..7683d1a --- /dev/null +++ b/src/create_or_update_branch.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +""" Create or Update Branch """ +import common as cmn +from git import Repo, GitCommandError +import os + + +CHERRYPICK_EMPTY = ( + "The previous cherry-pick is now empty, possibly due to conflict resolution." +) + + +def fetch_successful(repo, repo_url, branch): + try: + repo.git.fetch(repo_url, f"{branch}:refs/remotes/origin/{branch}") + except GitCommandError: + return False + return True + + +def is_ahead(repo, branch_1, branch_2): + # Return true if branch_2 is ahead of branch_1 + return ( + int(repo.git.rev_list("--right-only", "--count", f"{branch_1}...{branch_2}")) + > 0 + ) + + +def is_behind(repo, branch_1, branch_2): + # Return true if branch_2 is behind branch_1 + return ( + int(repo.git.rev_list("--left-only", "--count", f"{branch_1}...{branch_2}")) > 0 + ) + + +def is_even(repo, branch_1, branch_2): + # Return true if branch_2 is even with branch_1 + return not is_ahead(repo, branch_1, branch_2) and not is_behind( + repo, branch_1, branch_2 + ) + + +def has_diff(repo, branch_1, branch_2): + diff = repo.git.diff(f"{branch_1}..{branch_2}") + return len(diff) > 0 + + +def create_or_update_branch(repo, repo_url, commit_message, base, branch): + # Set the default return values + action = "none" + diff = False + + # Get the working base. This may or may not be the actual base. + working_base = repo.git.symbolic_ref("HEAD", "--short") + # If the base is not specified it is assumed to be the working base + if base is None: + base = working_base + + # Save the working base changes to a temporary branch + temp_branch = cmn.get_random_string(length=20) + repo.git.checkout("HEAD", b=temp_branch) + # Commit any uncomitted changes + if repo.is_dirty(untracked_files=True): + print(f"Uncommitted changes found. Adding a commit.") + repo.git.add("-A") + repo.git.commit(m=commit_message) + + # Perform fetch and reset the working base + # Commits made during the workflow will be removed + repo.git.fetch("--force", repo_url, f"{working_base}:{working_base}") + + # If the working base is not the base, rebase the temp branch commits + if working_base != base: + print( + f"Rebasing commits made to branch '{working_base}' on to base branch '{base}'" + ) + # Checkout the actual base + repo.git.fetch("--force", repo_url, f"{base}:{base}") + repo.git.checkout(base) + # Cherrypick commits from the temporary branch starting from the working base + commits = repo.git.rev_list("--reverse", f"{working_base}..{temp_branch}", ".") + for commit in commits.splitlines(): + try: + repo.git.cherry_pick( + "--strategy", + "recursive", + "--strategy-option", + "theirs", + f"{commit}", + ) + except GitCommandError as e: + if CHERRYPICK_EMPTY not in e.stderr: + print("Unexpected error: ", e) + raise + # Reset the temp branch to the working index + repo.git.checkout("-B", temp_branch, "HEAD") + # Reset the base + repo.git.fetch("--force", repo_url, f"{base}:{base}") + + # Try to fetch the pull request branch + if not fetch_successful(repo, repo_url, branch): + # The pull request branch does not exist + print(f"Pull request branch '{branch}' does not exist yet") + # Create the pull request branch + repo.git.checkout("HEAD", b=branch) + # Check if the pull request branch is ahead of the base + diff = is_ahead(repo, base, branch) + if diff: + action = "created" + print(f"Created branch '{branch}'") + else: + print( + f"Branch '{branch}' is not ahead of base '{base}' and will not be created" + ) + else: + # The pull request branch exists + print( + f"Pull request branch '{branch}' already exists as remote branch 'origin/{branch}'" + ) + # Checkout the pull request branch + repo.git.checkout(branch) + + if has_diff(repo, branch, temp_branch): + # If the branch differs from the recreated temp version then the branch is reset + # For changes on base this action is similar to a rebase of the pull request branch + print(f"Resetting '{branch}'") + repo.git.checkout("-B", branch, temp_branch) + # repo.git.switch("-C", branch, temp_branch) + + # Check if the pull request branch has been updated + # If the branch was reset or updated it will be ahead + # It may be behind if a reset now results in no diff with the base + if not is_even(repo, f"origin/{branch}", branch): + action = "updated" + print(f"Updated branch '{branch}'") + else: + print( + f"Branch '{branch}' is even with its remote and will not be updated" + ) + + # Check if the pull request branch is ahead of the base + diff = is_ahead(repo, base, branch) + + # Delete the temporary branch + repo.git.branch("--delete", "--force", temp_branch) + + return {"action": action, "diff": diff, "base": base} diff --git a/src/create_or_update_pull_request.py b/src/create_or_update_pull_request.py new file mode 100644 index 0000000..52b3101 --- /dev/null +++ b/src/create_or_update_pull_request.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +""" Create or Update Pull Request """ +from github import Github, GithubException +import os + + +def cs_string_to_list(str): + # Split the comma separated string into a list + l = [i.strip() for i in str.split(",")] + # Remove empty strings + return list(filter(None, l)) + + +def create_project_card(github_repo, project_name, project_column_name, pull_request): + # Locate the project by name + project = None + for project_item in github_repo.get_projects("all"): + if project_item.name == project_name: + project = project_item + break + + if not project: + print("::warning::Project not found. Unable to create project card.") + return + + # Locate the column by name + column = None + for column_item in project.get_columns(): + if column_item.name == project_column_name: + column = column_item + break + + if not column: + print("::warning::Project column not found. Unable to create project card.") + return + + # Create a project card for the pull request + column.create_card(content_id=pull_request.id, content_type="PullRequest") + print( + "Added pull request #%d to project '%s' under column '%s'" + % (pull_request.number, project.name, column.name) + ) + + +def create_or_update_pull_request( + github_token, + github_repository, + branch, + base, + title, + body, + labels, + assignees, + milestone, + reviewers, + team_reviewers, + project_name, + project_column_name, +): + # Create the pull request + github_repo = Github(github_token).get_repo(github_repository) + try: + pull_request = github_repo.create_pull( + title=title, body=body, base=base, head=branch + ) + print( + "Created pull request #%d (%s => %s)" % (pull_request.number, branch, base) + ) + except GithubException as e: + if e.status == 422: + # Format the branch name + head_branch = "%s:%s" % (github_repository.split("/")[0], branch) + # Get the pull request + pull_request = github_repo.get_pulls( + state="open", base=base, head=head_branch + )[0] + print( + "Updated pull request #%d (%s => %s)" + % (pull_request.number, branch, base) + ) + else: + print(str(e)) + raise + + # Set the output variables + os.system("echo ::set-env name=PULL_REQUEST_NUMBER::%d" % pull_request.number) + os.system("echo ::set-output name=pr_number::%d" % pull_request.number) + + # Set labels, assignees and milestone + if labels is not None: + print("Applying labels '%s'" % labels) + pull_request.as_issue().edit(labels=cs_string_to_list(labels)) + if assignees is not None: + print("Applying assignees '%s'" % assignees) + pull_request.as_issue().edit(assignees=cs_string_to_list(assignees)) + if milestone is not None: + print("Applying milestone '%s'" % milestone) + milestone = github_repo.get_milestone(int(milestone)) + pull_request.as_issue().edit(milestone=milestone) + + # Set pull request reviewers + if reviewers is not None: + print("Requesting reviewers '%s'" % reviewers) + try: + pull_request.create_review_request(reviewers=cs_string_to_list(reviewers)) + except GithubException as e: + # Likely caused by "Review cannot be requested from pull request + # author." + if e.status == 422: + print("Requesting reviewers failed - %s" % e.data["message"]) + + # Set pull request team reviewers + if team_reviewers is not None: + print("Requesting team reviewers '%s'" % team_reviewers) + pull_request.create_review_request( + team_reviewers=cs_string_to_list(team_reviewers) + ) + + # Create a project card for the pull request + if project_name is not None and project_column_name is not None: + try: + create_project_card( + github_repo, project_name, project_column_name, pull_request + ) + except GithubException as e: + # Likely caused by "Project already has the associated issue." + if e.status == 422: + print( + "Create project card failed - %s" % e.data["errors"][0]["message"] + ) diff --git a/src/create_pull_request.py b/src/create_pull_request.py new file mode 100755 index 0000000..31e602a --- /dev/null +++ b/src/create_pull_request.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +""" Create Pull Request """ +import common as cmn +import create_or_update_branch as coub +import create_or_update_pull_request as coupr +from git import Repo +import json +import os +import sys +import time + + +# Default the committer and author to the GitHub Actions bot +DEFAULT_COMMITTER = "GitHub " +DEFAULT_AUTHOR = ( + "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>" +) + + +def set_committer_author(repo, committer, author): + # When the user intends for the committer and author to be the same, + # ideally, just the committer should be supplied. When just the author + # is supplied, the same user intention is assumed. + if committer is None and author is not None: + print("Supplied author will also be used as the committer.") + committer = author + + # TODO Get committer and author from git config + # If just a committer exists, only set committer + # If just author exists also use for the committer + + # Set defaults if no committer/author has been supplied + if committer is None and author is None: + committer = DEFAULT_COMMITTER + author = DEFAULT_AUTHOR + + # Set git environment. This will not persist after the action completes. + committer_name, committer_email = cmn.parse_display_name_email(committer) + print(f"Configuring git committer as '{committer_name} <{committer_email}>'") + if author is not None: + author_name, author_email = cmn.parse_display_name_email(author) + print(f"Configuring git author as '{author_name} <{author_email}>'") + repo.git.update_environment( + GIT_COMMITTER_NAME=committer_name, + GIT_COMMITTER_EMAIL=committer_email, + GIT_AUTHOR_NAME=author_name, + GIT_AUTHOR_EMAIL=author_email, + ) + else: + repo.git.update_environment( + GIT_COMMITTER_NAME=committer_name, GIT_COMMITTER_EMAIL=committer_email, + ) + + +# Get required environment variables +github_token = os.environ["GITHUB_TOKEN"] +github_repository = os.environ["GITHUB_REPOSITORY"] +# Get environment variables with defaults +branch = os.getenv("CPR_BRANCH", "create-pull-request/patch") +commit_message = os.getenv( + "CPR_COMMIT_MESSAGE", "Changes by create-pull-request action" +) +# Get environment variables with a default of 'None' +committer = os.environ.get("CPR_COMMITTER") +author = os.environ.get("CPR_AUTHOR") +base = os.environ.get("CPR_BASE") + +# Set the repo to the working directory +repo = Repo(os.getcwd()) + +# Determine if the checked out ref is a valid base for a pull request +# The action needs the checked out HEAD ref to be a branch +# This check will fail in the following cases: +# - HEAD is detached +# - HEAD is a merge commit (pull_request events) +# - HEAD is a tag +try: + working_base = repo.git.symbolic_ref("HEAD", "--short") +except: + print(f"::debug::{working_base}") + print( + f"::error::The checked out ref is not a valid base for a pull request. " + + "Unable to continue. Exiting." + ) + sys.exit(1) + +# Exit if the working base is a PR branch created by this action. +# This may occur when using a PAT instead of GITHUB_TOKEN because +# a PAT allows workflow actions to trigger further events. +if working_base.startswith(branch): + print( + f"::error::Working base branch '{working_base}' was created by this action. " + + "Unable to continue. Exiting." + ) + sys.exit(1) + +# Fetch an optional environment variable to determine the branch suffix +branch_suffix = os.environ.get("CPR_BRANCH_SUFFIX") +if branch_suffix is not None: + if branch_suffix == "short-commit-hash": + # Suffix with the short SHA1 hash + branch = "{}-{}".format(branch, repo.git.rev_parse("--short", "HEAD")) + elif branch_suffix == "timestamp": + # Suffix with the current timestamp + branch = "{}-{}".format(branch, int(time.time())) + elif branch_suffix == "random": + # Suffix with a 7 character random string + branch = "{}-{}".format(branch, cmn.get_random_string()) + else: + print( + f"::error::Branch suffix '{branch_suffix}' is not a valid value. " + + "Unable to continue. Exiting." + ) + sys.exit(1) + +# Output head branch +print(f"Pull request branch to create or update set to '{branch}'") + +# Set the committer and author +try: + set_committer_author(repo, committer, author) +except ValueError as e: + print(f"::error::{e} " + "Unable to continue. Exiting.") + sys.exit(1) + +# Set the repository URL +repo_url = f"https://x-access-token:{github_token}@github.com/{github_repository}" + +# Create or update the pull request branch +result = coub.create_or_update_branch(repo, repo_url, commit_message, base, branch) + +if result["action"] in ["created", "updated"]: + # The branch was created or updated + print(f"Pushing pull request branch to 'origin/{branch}'") + repo.git.push("--force", repo_url, f"HEAD:refs/heads/{branch}") + + # Set the base. It would have been 'None' if not specified as an input + base = result["base"] + + # TODO Figure out what to do when there is no diff with the base anymore + # if not result["diff"]: + + # Fetch optional environment variables with default values + title = os.getenv("CPR_TITLE", "Auto-generated by create-pull-request action") + body = os.getenv( + "CPR_BODY", + "Auto-generated pull request by " + "[create-pull-request](https://github.com/peter-evans/create-pull-request) GitHub Action", + ) + + # Create or update the pull request + coupr.create_or_update_pull_request( + github_token, + github_repository, + branch, + base, + title, + body, + os.environ.get("CPR_LABELS"), + os.environ.get("CPR_ASSIGNEES"), + os.environ.get("CPR_MILESTONE"), + os.environ.get("CPR_REVIEWERS"), + os.environ.get("CPR_TEAM_REVIEWERS"), + os.environ.get("CPR_PROJECT_NAME"), + os.environ.get("CPR_PROJECT_COLUMN_NAME"), + ) diff --git a/src/test_common.py b/src/test_common.py new file mode 100644 index 0000000..2d7dd2b --- /dev/null +++ b/src/test_common.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +""" Test Common """ +import common as cmn +import pytest + + +def test_get_random_string(): + assert len(cmn.get_random_string()) == 7 + assert len(cmn.get_random_string(length=20)) == 20 + + +def test_parse_display_name_email_success(): + name, email = cmn.parse_display_name_email("abc def ") + assert name == "abc def" + assert email == "abc@def.com" + + name, email = cmn.parse_display_name_email( + "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>" + ) + assert name == "github-actions[bot]" + assert email == "41898282+github-actions[bot]@users.noreply.github.com" + + +def test_parse_display_name_email_failure(): + display_name_email = "abc@def.com" + with pytest.raises(ValueError) as e_info: + cmn.parse_display_name_email(display_name_email) + assert ( + e_info.value.args[0] + == f"The format of '{display_name_email}' is not a valid email address with display name" + ) + + display_name_email = " < >" + with pytest.raises(ValueError) as e_info: + cmn.parse_display_name_email(display_name_email) + assert ( + e_info.value.args[0] + == f"The format of '{display_name_email}' is not a valid email address with display name" + ) diff --git a/src/test_create_or_update_branch.py b/src/test_create_or_update_branch.py new file mode 100644 index 0000000..c6ff95b --- /dev/null +++ b/src/test_create_or_update_branch.py @@ -0,0 +1,748 @@ +#!/usr/bin/env python3 +""" Test Create or Update Branch """ +import create_or_update_branch as coub +from git import Repo +import os +import pytest +import sys +import time + + +author_name = "github-actions[bot]" +author_email = "41898282+github-actions[bot]@users.noreply.github.com" +committer_name = "GitHub" +committer_email = "noreply@github.com" + +# Set git environment +repo = Repo(os.getcwd()) +repo.git.update_environment( + GIT_AUTHOR_NAME=author_name, + GIT_AUTHOR_EMAIL=author_email, + GIT_COMMITTER_NAME=committer_name, + GIT_COMMITTER_EMAIL=committer_email, +) + +REPO_URL = repo.git.config("--get", "remote.origin.url") + +TRACKED_FILE = "tracked-file.txt" +UNTRACKED_FILE = "untracked-file.txt" + +DEFAULT_BRANCH = "tests/master" +NOT_BASE_BRANCH = "tests/branch-that-is-not-the-base" +NOT_EXIST_BRANCH = "tests/branch-that-does-not-exist" + +COMMIT_MESSAGE = "Changes by create-pull-request action" +BRANCH = "tests/create-pull-request/patch" +BASE = DEFAULT_BRANCH + + +def create_tracked_change(content=None): + if content is None: + content = str(time.time()) + # Create a tracked file change + with open(TRACKED_FILE, "w") as f: + f.write(content) + return content + + +def create_untracked_change(content=None): + if content is None: + content = str(time.time()) + # Create an untracked file change + with open(UNTRACKED_FILE, "w") as f: + f.write(content) + return content + + +def get_tracked_content(): + # Read the content of the tracked file + with open(TRACKED_FILE, "r") as f: + return f.read() + + +def get_untracked_content(): + # Read the content of the untracked file + with open(UNTRACKED_FILE, "r") as f: + return f.read() + + +def create_changes(tracked_content=None, untracked_content=None): + tracked_content = create_tracked_change(tracked_content) + untracked_content = create_untracked_change(untracked_content) + return tracked_content, untracked_content + + +def create_commits(number=2, final_tracked_content=None, final_untracked_content=None): + for i in range(number): + commit_number = i + 1 + if commit_number == number: + tracked_content, untracked_content = create_changes( + final_tracked_content, final_untracked_content + ) + else: + tracked_content, untracked_content = create_changes() + repo.git.add("-A") + repo.git.commit(m=f"Commit {commit_number}") + return tracked_content, untracked_content + + +@pytest.fixture(scope="module", autouse=True) +def before_after_all(): + print("Before all tests") + # Check there are no local changes that might be + # destroyed by running these tests + assert not repo.is_dirty(untracked_files=True) + + # Create a new default branch for the test run + repo.git.checkout("master") + repo.git.checkout("HEAD", b=NOT_BASE_BRANCH) + create_tracked_change() + repo.git.add("-A") + repo.git.commit(m="This commit should not appear in pr branches") + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{NOT_BASE_BRANCH}") + # Create a new default branch for the test run + repo.git.checkout("master") + repo.git.checkout("HEAD", b=DEFAULT_BRANCH) + create_tracked_change() + repo.git.add("-A") + repo.git.commit(m="Add file to be a tracked file for tests") + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{DEFAULT_BRANCH}") + + yield + + print("After all tests") + repo.git.checkout("master") + # Delete the "not base branch" created for the test run + repo.git.branch("--delete", "--force", NOT_BASE_BRANCH) + repo.git.push("--delete", "--force", REPO_URL, f"refs/heads/{NOT_BASE_BRANCH}") + # Delete the default branch created for the test run + repo.git.branch("--delete", "--force", DEFAULT_BRANCH) + repo.git.push("--delete", "--force", REPO_URL, f"refs/heads/{DEFAULT_BRANCH}") + + +def before_test(): + print("Before test") + # Checkout the default branch + repo.git.checkout(DEFAULT_BRANCH) + + +def after_test(delete_remote=True): + print("After test") + # Output git log + print(repo.git.log("-5", pretty="oneline")) + # Delete the pull request branch if it exists + repo.git.checkout(DEFAULT_BRANCH) + print(f"Deleting {BRANCH}") + for branch in repo.branches: + if branch.name == BRANCH: + repo.git.branch("--delete", "--force", BRANCH) + break + if delete_remote: + print(f"Deleting origin/{BRANCH}") + for ref in repo.remotes.origin.refs: + if ref.name == f"origin/{BRANCH}": + repo.git.push("--delete", "--force", REPO_URL, f"refs/heads/{BRANCH}") + repo.remotes.origin.fetch("--prune") + break + + +@pytest.fixture(autouse=True) +def before_after_tests(): + before_test() + yield + after_test() + + +# Tests if a branch exists and can be fetched +def coub_fetch_successful(): + assert coub.fetch_successful(repo, REPO_URL, NOT_BASE_BRANCH) + assert not coub.fetch_successful(repo, REPO_URL, NOT_EXIST_BRANCH) + + +# Tests no changes resulting in no new branch being created +def coub_no_changes_on_create(): + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH) + assert result["action"] == "none" + + +# Tests create and update with a tracked file change +def coub_tracked_changes(): + # Create a tracked file change + tracked_content = create_tracked_change() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH) + assert result["action"] == "created" + assert get_tracked_content() == tracked_content + + # Push pull request branch to remote + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}") + repo.remotes.origin.fetch() + + after_test(delete_remote=False) + before_test() + + # Create a tracked file change + tracked_content = create_tracked_change() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH) + assert result["action"] == "updated" + assert result["diff"] + assert get_tracked_content() == tracked_content + + +# Tests create and update with an untracked file change +def coub_untracked_changes(): + # Create an untracked file change + untracked_content = create_untracked_change() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH) + assert result["action"] == "created" + assert get_untracked_content() == untracked_content + + # Push pull request branch to remote + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}") + repo.remotes.origin.fetch() + + after_test(delete_remote=False) + before_test() + + # Create an untracked file change + untracked_content = create_untracked_change() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH) + assert result["action"] == "updated" + assert result["diff"] + assert get_untracked_content() == untracked_content + + +# Tests create and update with identical changes +# The pull request branch will not be updated +def coub_identical_changes(): + # Create tracked and untracked file changes + tracked_content, untracked_content = create_changes() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH) + assert result["action"] == "created" + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + # Push pull request branch to remote + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}") + repo.remotes.origin.fetch() + + after_test(delete_remote=False) + before_test() + + # Create identical tracked and untracked file changes + create_changes(tracked_content, untracked_content) + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH) + assert result["action"] == "none" + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + +# Tests create and update with commits on the base inbetween +def coub_commits_on_base(): + # Create tracked and untracked file changes + tracked_content, untracked_content = create_changes() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH) + assert result["action"] == "created" + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + # Push pull request branch to remote + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}") + repo.remotes.origin.fetch() + + after_test(delete_remote=False) + before_test() + + # Create commits on the base + create_commits() + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{DEFAULT_BRANCH}") + repo.remotes.origin.fetch() + + # Create tracked and untracked file changes + tracked_content, untracked_content = create_changes() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH) + assert result["action"] == "updated" + assert result["diff"] + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + +# Tests create and then an update with no changes +# This effectively reverts the branch back to match the base and results in no diff +def coub_changes_no_diff(): + # Save the default branch tracked content + default_tracked_content = get_tracked_content() + + # Create tracked and untracked file changes + tracked_content, untracked_content = create_changes() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH) + assert result["action"] == "created" + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + # Push pull request branch to remote + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}") + repo.remotes.origin.fetch() + + after_test(delete_remote=False) + before_test() + + # Running with no update effectively reverts the branch back to match the base + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH) + assert result["action"] == "updated" + assert result["diff"] == False + assert get_tracked_content() == default_tracked_content + + +# Tests create and update with commits on the base inbetween +# The changes on base effectively revert the branch back to match the base and results in no diff +def coub_commits_on_base_no_diff(): + # Create tracked and untracked file changes + tracked_content, untracked_content = create_changes() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH) + assert result["action"] == "created" + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + # Push pull request branch to remote + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}") + repo.remotes.origin.fetch() + + after_test(delete_remote=False) + before_test() + + # Create commits on the base + tracked_content, untracked_content = create_commits() + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{DEFAULT_BRANCH}") + repo.remotes.origin.fetch() + + # Create the same tracked and untracked file changes that were made to the base + create_changes(tracked_content, untracked_content) + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH) + assert result["action"] == "updated" + assert result["diff"] == False + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + +# Tests create and update with commits on the working base (during the workflow) +def coub_commits_on_working_base(): + # Create commits on the working base + tracked_content, untracked_content = create_commits() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH) + assert result["action"] == "created" + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + # Push pull request branch to remote + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}") + repo.remotes.origin.fetch() + + after_test(delete_remote=False) + before_test() + + # Create commits on the working base + tracked_content, untracked_content = create_commits() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH) + assert result["action"] == "updated" + assert result["diff"] + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + +# Tests create and update with changes and commits on the working base (during the workflow) +def coub_changes_and_commits_on_working_base(): + # Create commits on the working base + create_commits() + # Create tracked and untracked file changes + tracked_content, untracked_content = create_changes() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH) + assert result["action"] == "created" + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + # Push pull request branch to remote + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}") + repo.remotes.origin.fetch() + + after_test(delete_remote=False) + before_test() + + # Create commits on the working base + create_commits() + # Create tracked and untracked file changes + tracked_content, untracked_content = create_changes() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH) + assert result["action"] == "updated" + assert result["diff"] + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + +# Tests create and update with changes and commits on the working base (during the workflow) +# with commits on the base inbetween +def coub_changes_and_commits_on_base_and_working_base(): + # Create commits on the working base + create_commits() + # Create tracked and untracked file changes + tracked_content, untracked_content = create_changes() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH) + assert result["action"] == "created" + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + # Push pull request branch to remote + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}") + repo.remotes.origin.fetch() + + after_test(delete_remote=False) + before_test() + + # Create commits on the base + create_commits() + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{DEFAULT_BRANCH}") + repo.remotes.origin.fetch() + + # Create commits on the working base + create_commits() + # Create tracked and untracked file changes + tracked_content, untracked_content = create_changes() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH) + assert result["action"] == "updated" + assert result["diff"] + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + +# Working Base is Not Base (WBNB) +# Tests no changes resulting in no new branch being created +def coub_wbnb_no_changes_on_create(): + # Set the working base to a branch that is not the pull request base + repo.git.checkout(NOT_BASE_BRANCH) + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH) + assert result["action"] == "none" + + +# Working Base is Not Base (WBNB) +# Tests create and update with a tracked file change +def coub_wbnb_tracked_changes(): + # Set the working base to a branch that is not the pull request base + repo.git.checkout(NOT_BASE_BRANCH) + # Create a tracked file change + tracked_content = create_tracked_change() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH) + assert result["action"] == "created" + assert get_tracked_content() == tracked_content + + # Push pull request branch to remote + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}") + repo.remotes.origin.fetch() + + after_test(delete_remote=False) + before_test() + + # Set the working base to a branch that is not the pull request base + repo.git.checkout(NOT_BASE_BRANCH) + # Create a tracked file change + tracked_content = create_tracked_change() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH) + assert result["action"] == "updated" + assert result["diff"] + assert get_tracked_content() == tracked_content + + +# Working Base is Not Base (WBNB) +# Tests create and update with an untracked file change +def coub_wbnb_untracked_changes(): + # Set the working base to a branch that is not the pull request base + repo.git.checkout(NOT_BASE_BRANCH) + # Create an untracked file change + untracked_content = create_untracked_change() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH) + assert result["action"] == "created" + assert get_untracked_content() == untracked_content + + # Push pull request branch to remote + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}") + repo.remotes.origin.fetch() + + after_test(delete_remote=False) + before_test() + + # Set the working base to a branch that is not the pull request base + repo.git.checkout(NOT_BASE_BRANCH) + # Create an untracked file change + untracked_content = create_untracked_change() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH) + assert result["action"] == "updated" + assert result["diff"] + assert get_untracked_content() == untracked_content + + +# Working Base is Not Base (WBNB) +# Tests create and update with identical changes +# The pull request branch will not be updated +def coub_wbnb_identical_changes(): + # Set the working base to a branch that is not the pull request base + repo.git.checkout(NOT_BASE_BRANCH) + # Create tracked and untracked file changes + tracked_content, untracked_content = create_changes() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH) + assert result["action"] == "created" + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + # Push pull request branch to remote + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}") + repo.remotes.origin.fetch() + + after_test(delete_remote=False) + before_test() + + # Set the working base to a branch that is not the pull request base + repo.git.checkout(NOT_BASE_BRANCH) + # Create identical tracked and untracked file changes + create_changes(tracked_content, untracked_content) + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH) + assert result["action"] == "none" + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + +# Working Base is Not Base (WBNB) +# Tests create and update with commits on the base inbetween +def coub_wbnb_commits_on_base(): + # Set the working base to a branch that is not the pull request base + repo.git.checkout(NOT_BASE_BRANCH) + # Create tracked and untracked file changes + tracked_content, untracked_content = create_changes() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH) + assert result["action"] == "created" + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + # Push pull request branch to remote + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}") + repo.remotes.origin.fetch() + + after_test(delete_remote=False) + before_test() + + # Create commits on the base + create_commits() + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{DEFAULT_BRANCH}") + repo.remotes.origin.fetch() + + # Set the working base to a branch that is not the pull request base + repo.git.checkout(NOT_BASE_BRANCH) + # Create tracked and untracked file changes + tracked_content, untracked_content = create_changes() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH) + assert result["action"] == "updated" + assert result["diff"] + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + +# Working Base is Not Base (WBNB) +# Tests create and then an update with no changes +# This effectively reverts the branch back to match the base and results in no diff +def coub_wbnb_changes_no_diff(): + # Save the default branch tracked content + default_tracked_content = get_tracked_content() + # Set the working base to a branch that is not the pull request base + repo.git.checkout(NOT_BASE_BRANCH) + # Create tracked and untracked file changes + tracked_content, untracked_content = create_changes() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH) + assert result["action"] == "created" + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + # Push pull request branch to remote + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}") + repo.remotes.origin.fetch() + + after_test(delete_remote=False) + before_test() + + # Set the working base to a branch that is not the pull request base + repo.git.checkout(NOT_BASE_BRANCH) + # Running with no update effectively reverts the branch back to match the base + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH) + assert result["action"] == "updated" + assert result["diff"] == False + assert get_tracked_content() == default_tracked_content + + +# Working Base is Not Base (WBNB) +# Tests create and update with commits on the base inbetween +# The changes on base effectively revert the branch back to match the base and results in no diff +# This scenario will cause cherrypick to fail due to an empty commit. +# The commit is empty because the changes now exist on the base. +def coub_wbnb_commits_on_base_no_diff(): + # Set the working base to a branch that is not the pull request base + repo.git.checkout(NOT_BASE_BRANCH) + # Create tracked and untracked file changes + tracked_content, untracked_content = create_changes() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH) + assert result["action"] == "created" + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + # Push pull request branch to remote + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}") + repo.remotes.origin.fetch() + + after_test(delete_remote=False) + before_test() + + # Create commits on the base + tracked_content, untracked_content = create_commits() + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{DEFAULT_BRANCH}") + repo.remotes.origin.fetch() + + # Set the working base to a branch that is not the pull request base + repo.git.checkout(NOT_BASE_BRANCH) + # Create the same tracked and untracked file changes that were made to the base + create_changes(tracked_content, untracked_content) + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH) + assert result["action"] == "updated" + assert result["diff"] == False + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + +# Working Base is Not Base (WBNB) +# Tests create and update with commits on the working base (during the workflow) +def coub_wbnb_commits_on_working_base(): + # Set the working base to a branch that is not the pull request base + repo.git.checkout(NOT_BASE_BRANCH) + # Create commits on the working base + tracked_content, untracked_content = create_commits() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH) + assert result["action"] == "created" + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + # Push pull request branch to remote + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}") + repo.remotes.origin.fetch() + + after_test(delete_remote=False) + before_test() + + # Set the working base to a branch that is not the pull request base + repo.git.checkout(NOT_BASE_BRANCH) + # Create commits on the working base + tracked_content, untracked_content = create_commits() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH) + assert result["action"] == "updated" + assert result["diff"] + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + +# Working Base is Not Base (WBNB) +# Tests create and update with changes and commits on the working base (during the workflow) +def coub_wbnb_changes_and_commits_on_working_base(): + # Set the working base to a branch that is not the pull request base + repo.git.checkout(NOT_BASE_BRANCH) + # Create commits on the working base + create_commits() + # Create tracked and untracked file changes + tracked_content, untracked_content = create_changes() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH) + assert result["action"] == "created" + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + # Push pull request branch to remote + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}") + repo.remotes.origin.fetch() + + after_test(delete_remote=False) + before_test() + + # Set the working base to a branch that is not the pull request base + repo.git.checkout(NOT_BASE_BRANCH) + # Create commits on the working base + create_commits() + # Create tracked and untracked file changes + tracked_content, untracked_content = create_changes() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH) + assert result["action"] == "updated" + assert result["diff"] + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + +# Working Base is Not Base (WBNB) +# Tests create and update with changes and commits on the working base (during the workflow) +# with commits on the base inbetween +def coub_wbnb_changes_and_commits_on_base_and_working_base(): + # Set the working base to a branch that is not the pull request base + repo.git.checkout(NOT_BASE_BRANCH) + # Create commits on the working base + create_commits() + # Create tracked and untracked file changes + tracked_content, untracked_content = create_changes() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH) + assert result["action"] == "created" + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + # Push pull request branch to remote + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}") + repo.remotes.origin.fetch() + + after_test(delete_remote=False) + before_test() + + # Create commits on the base + create_commits() + repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{DEFAULT_BRANCH}") + repo.remotes.origin.fetch() + + # Set the working base to a branch that is not the pull request base + repo.git.checkout(NOT_BASE_BRANCH) + # Create commits on the working base + create_commits() + # Create tracked and untracked file changes + tracked_content, untracked_content = create_changes() + result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH) + assert result["action"] == "updated" + assert result["diff"] + assert get_tracked_content() == tracked_content + assert get_untracked_content() == untracked_content + + +# pytest -v -s ~/git/create-pull-request/src + +# test_coub_fetch_successful = coub_fetch_successful + +# test_coub_no_changes_on_create = coub_no_changes_on_create +# test_coub_tracked_changes = coub_tracked_changes +# test_coub_untracked_changes = coub_untracked_changes +# test_coub_identical_changes = coub_identical_changes +# test_coub_commits_on_base = coub_commits_on_base + +# test_coub_changes_no_diff = coub_changes_no_diff +# test_coub_commits_on_base_no_diff = coub_commits_on_base_no_diff + +# test_coub_commits_on_working_base = coub_commits_on_working_base +# test_coub_changes_and_commits_on_working_base = coub_changes_and_commits_on_working_base +# test_coub_changes_and_commits_on_base_and_working_base = coub_changes_and_commits_on_base_and_working_base + +# # WBNB +# test_coub_wbnb_no_changes_on_create = coub_wbnb_no_changes_on_create +# test_coub_wbnb_tracked_changes = coub_wbnb_tracked_changes +# test_coub_wbnb_untracked_changes = coub_wbnb_untracked_changes +# test_coub_wbnb_identical_changes = coub_wbnb_identical_changes +# test_coub_wbnb_commits_on_base = coub_wbnb_commits_on_base + +# test_coub_wbnb_changes_no_diff = coub_wbnb_changes_no_diff +# test_coub_wbnb_commits_on_base_no_diff = coub_wbnb_commits_on_base_no_diff + +# test_coub_wbnb_commits_on_working_base = coub_wbnb_commits_on_working_base +# test_coub_wbnb_changes_and_commits_on_working_base = coub_wbnb_changes_and_commits_on_working_base +# test_coub_wbnb_changes_and_commits_on_base_and_working_base = coub_wbnb_changes_and_commits_on_base_and_working_base From efbd4fa1efb0da883004e36dc755119e29f15758 Mon Sep 17 00:00:00 2001 From: Peter Evans Date: Fri, 27 Dec 2019 15:32:37 +0900 Subject: [PATCH 02/30] Fix setting author and committer --- dist/src/create_pull_request.py | 37 +++++++++++++++------------------ src/create_pull_request.py | 37 +++++++++++++++------------------ 2 files changed, 34 insertions(+), 40 deletions(-) diff --git a/dist/src/create_pull_request.py b/dist/src/create_pull_request.py index 31e602a..18cd017 100755 --- a/dist/src/create_pull_request.py +++ b/dist/src/create_pull_request.py @@ -18,16 +18,18 @@ DEFAULT_AUTHOR = ( def set_committer_author(repo, committer, author): - # When the user intends for the committer and author to be the same, - # ideally, just the committer should be supplied. When just the author - # is supplied, the same user intention is assumed. + # If either committer or author is supplied they will be cross used if committer is None and author is not None: print("Supplied author will also be used as the committer.") committer = author + if author is None and committer is not None: + print("Supplied committer will also be used as the author.") + author = committer - # TODO Get committer and author from git config - # If just a committer exists, only set committer - # If just author exists also use for the committer + # TODO If no overrides have been supplied, check if already set in config + # If not set, continue to use defaults + # If set, return + # https://git-scm.com/docs/git-config#Documentation/git-config.txt-username # Set defaults if no committer/author has been supplied if committer is None and author is None: @@ -36,20 +38,15 @@ def set_committer_author(repo, committer, author): # Set git environment. This will not persist after the action completes. committer_name, committer_email = cmn.parse_display_name_email(committer) - print(f"Configuring git committer as '{committer_name} <{committer_email}>'") - if author is not None: - author_name, author_email = cmn.parse_display_name_email(author) - print(f"Configuring git author as '{author_name} <{author_email}>'") - repo.git.update_environment( - GIT_COMMITTER_NAME=committer_name, - GIT_COMMITTER_EMAIL=committer_email, - GIT_AUTHOR_NAME=author_name, - GIT_AUTHOR_EMAIL=author_email, - ) - else: - repo.git.update_environment( - GIT_COMMITTER_NAME=committer_name, GIT_COMMITTER_EMAIL=committer_email, - ) + author_name, author_email = cmn.parse_display_name_email(author) + repo.git.update_environment( + GIT_COMMITTER_NAME=committer_name, + GIT_COMMITTER_EMAIL=committer_email, + GIT_AUTHOR_NAME=author_name, + GIT_AUTHOR_EMAIL=author_email, + ) + print(f"Configured git committer as '{committer_name} <{committer_email}>'") + print(f"Configured git author as '{author_name} <{author_email}>'") # Get required environment variables diff --git a/src/create_pull_request.py b/src/create_pull_request.py index 31e602a..18cd017 100755 --- a/src/create_pull_request.py +++ b/src/create_pull_request.py @@ -18,16 +18,18 @@ DEFAULT_AUTHOR = ( def set_committer_author(repo, committer, author): - # When the user intends for the committer and author to be the same, - # ideally, just the committer should be supplied. When just the author - # is supplied, the same user intention is assumed. + # If either committer or author is supplied they will be cross used if committer is None and author is not None: print("Supplied author will also be used as the committer.") committer = author + if author is None and committer is not None: + print("Supplied committer will also be used as the author.") + author = committer - # TODO Get committer and author from git config - # If just a committer exists, only set committer - # If just author exists also use for the committer + # TODO If no overrides have been supplied, check if already set in config + # If not set, continue to use defaults + # If set, return + # https://git-scm.com/docs/git-config#Documentation/git-config.txt-username # Set defaults if no committer/author has been supplied if committer is None and author is None: @@ -36,20 +38,15 @@ def set_committer_author(repo, committer, author): # Set git environment. This will not persist after the action completes. committer_name, committer_email = cmn.parse_display_name_email(committer) - print(f"Configuring git committer as '{committer_name} <{committer_email}>'") - if author is not None: - author_name, author_email = cmn.parse_display_name_email(author) - print(f"Configuring git author as '{author_name} <{author_email}>'") - repo.git.update_environment( - GIT_COMMITTER_NAME=committer_name, - GIT_COMMITTER_EMAIL=committer_email, - GIT_AUTHOR_NAME=author_name, - GIT_AUTHOR_EMAIL=author_email, - ) - else: - repo.git.update_environment( - GIT_COMMITTER_NAME=committer_name, GIT_COMMITTER_EMAIL=committer_email, - ) + author_name, author_email = cmn.parse_display_name_email(author) + repo.git.update_environment( + GIT_COMMITTER_NAME=committer_name, + GIT_COMMITTER_EMAIL=committer_email, + GIT_AUTHOR_NAME=author_name, + GIT_AUTHOR_EMAIL=author_email, + ) + print(f"Configured git committer as '{committer_name} <{committer_email}>'") + print(f"Configured git author as '{author_name} <{author_email}>'") # Get required environment variables From 4f8d5b9d3e0fa4e3f5ad62552caa8559a299b4f1 Mon Sep 17 00:00:00 2001 From: Peter Evans Date: Fri, 27 Dec 2019 18:29:33 +0900 Subject: [PATCH 03/30] Add a check for user config set in the workflow --- dist/src/create_or_update_branch.py | 4 +-- dist/src/create_pull_request.py | 46 +++++++++++++++++++++++++---- src/create_or_update_branch.py | 4 +-- src/create_pull_request.py | 46 +++++++++++++++++++++++++---- 4 files changed, 84 insertions(+), 16 deletions(-) diff --git a/dist/src/create_or_update_branch.py b/dist/src/create_or_update_branch.py index 7683d1a..2cb24d1 100644 --- a/dist/src/create_or_update_branch.py +++ b/dist/src/create_or_update_branch.py @@ -134,9 +134,7 @@ def create_or_update_branch(repo, repo_url, commit_message, base, branch): action = "updated" print(f"Updated branch '{branch}'") else: - print( - f"Branch '{branch}' is even with its remote and will not be updated" - ) + print(f"Branch '{branch}' is even with its remote and will not be updated") # Check if the pull request branch is ahead of the base diff = is_ahead(repo, base, branch) diff --git a/dist/src/create_pull_request.py b/dist/src/create_pull_request.py index 18cd017..b0723e9 100755 --- a/dist/src/create_pull_request.py +++ b/dist/src/create_pull_request.py @@ -3,7 +3,7 @@ import common as cmn import create_or_update_branch as coub import create_or_update_pull_request as coupr -from git import Repo +from git import Repo, GitCommandError import json import os import sys @@ -17,6 +17,41 @@ DEFAULT_AUTHOR = ( ) +def get_git_config_value(repo, name): + try: + return repo.git.config("--get", name) + except GitCommandError: + return None + + +def git_user_config_is_set(repo): + name = get_git_config_value(repo, "user.name") + email = get_git_config_value(repo, "user.email") + + if name is not None and email is not None: + print(f"Git user already configured as '{name} <{email}>'") + return True + + committer_name = get_git_config_value(repo, "committer.name") + committer_email = get_git_config_value(repo, "committer.email") + author_name = get_git_config_value(repo, "author.name") + author_email = get_git_config_value(repo, "author.email") + + if ( + committer_name is not None + and committer_email is not None + and author_name is not None + and author_email is not None + ): + print( + f"Git committer already configured as '{committer_name} <{committer_email}>'" + ) + print(f"Git author already configured as '{author_name} <{author_email}>'") + return True + + return False + + def set_committer_author(repo, committer, author): # If either committer or author is supplied they will be cross used if committer is None and author is not None: @@ -26,10 +61,11 @@ def set_committer_author(repo, committer, author): print("Supplied committer will also be used as the author.") author = committer - # TODO If no overrides have been supplied, check if already set in config - # If not set, continue to use defaults - # If set, return - # https://git-scm.com/docs/git-config#Documentation/git-config.txt-username + # If no committer/author has been supplied but user configuration already + # exists in git config we can exit and use the existing config as-is. + if committer is None and author is None: + if git_user_config_is_set(repo): + return # Set defaults if no committer/author has been supplied if committer is None and author is None: diff --git a/src/create_or_update_branch.py b/src/create_or_update_branch.py index 7683d1a..2cb24d1 100644 --- a/src/create_or_update_branch.py +++ b/src/create_or_update_branch.py @@ -134,9 +134,7 @@ def create_or_update_branch(repo, repo_url, commit_message, base, branch): action = "updated" print(f"Updated branch '{branch}'") else: - print( - f"Branch '{branch}' is even with its remote and will not be updated" - ) + print(f"Branch '{branch}' is even with its remote and will not be updated") # Check if the pull request branch is ahead of the base diff = is_ahead(repo, base, branch) diff --git a/src/create_pull_request.py b/src/create_pull_request.py index 18cd017..b0723e9 100755 --- a/src/create_pull_request.py +++ b/src/create_pull_request.py @@ -3,7 +3,7 @@ import common as cmn import create_or_update_branch as coub import create_or_update_pull_request as coupr -from git import Repo +from git import Repo, GitCommandError import json import os import sys @@ -17,6 +17,41 @@ DEFAULT_AUTHOR = ( ) +def get_git_config_value(repo, name): + try: + return repo.git.config("--get", name) + except GitCommandError: + return None + + +def git_user_config_is_set(repo): + name = get_git_config_value(repo, "user.name") + email = get_git_config_value(repo, "user.email") + + if name is not None and email is not None: + print(f"Git user already configured as '{name} <{email}>'") + return True + + committer_name = get_git_config_value(repo, "committer.name") + committer_email = get_git_config_value(repo, "committer.email") + author_name = get_git_config_value(repo, "author.name") + author_email = get_git_config_value(repo, "author.email") + + if ( + committer_name is not None + and committer_email is not None + and author_name is not None + and author_email is not None + ): + print( + f"Git committer already configured as '{committer_name} <{committer_email}>'" + ) + print(f"Git author already configured as '{author_name} <{author_email}>'") + return True + + return False + + def set_committer_author(repo, committer, author): # If either committer or author is supplied they will be cross used if committer is None and author is not None: @@ -26,10 +61,11 @@ def set_committer_author(repo, committer, author): print("Supplied committer will also be used as the author.") author = committer - # TODO If no overrides have been supplied, check if already set in config - # If not set, continue to use defaults - # If set, return - # https://git-scm.com/docs/git-config#Documentation/git-config.txt-username + # If no committer/author has been supplied but user configuration already + # exists in git config we can exit and use the existing config as-is. + if committer is None and author is None: + if git_user_config_is_set(repo): + return # Set defaults if no committer/author has been supplied if committer is None and author is None: From b3805d65e30963ed59e7c0338e63e02f9203a3b5 Mon Sep 17 00:00:00 2001 From: Peter Evans Date: Sat, 28 Dec 2019 17:09:58 +0900 Subject: [PATCH 04/30] Update string formatting --- dist/src/create_or_update_pull_request.py | 36 ++++++++++------------- src/create_or_update_pull_request.py | 36 ++++++++++------------- 2 files changed, 32 insertions(+), 40 deletions(-) diff --git a/dist/src/create_or_update_pull_request.py b/dist/src/create_or_update_pull_request.py index 52b3101..009be66 100644 --- a/dist/src/create_or_update_pull_request.py +++ b/dist/src/create_or_update_pull_request.py @@ -63,55 +63,49 @@ def create_or_update_pull_request( pull_request = github_repo.create_pull( title=title, body=body, base=base, head=branch ) - print( - "Created pull request #%d (%s => %s)" % (pull_request.number, branch, base) - ) + print(f"Created pull request #{pull_request.number} ({branch} => {base})") except GithubException as e: if e.status == 422: - # Format the branch name - head_branch = "%s:%s" % (github_repository.split("/")[0], branch) + # A pull request exists for this branch and base + head_branch = "{}:{}".format(github_repository.split("/")[0], branch) # Get the pull request pull_request = github_repo.get_pulls( state="open", base=base, head=head_branch )[0] - print( - "Updated pull request #%d (%s => %s)" - % (pull_request.number, branch, base) - ) + print(f"Updated pull request #{pull_request.number} ({branch} => {base})") else: print(str(e)) raise # Set the output variables - os.system("echo ::set-env name=PULL_REQUEST_NUMBER::%d" % pull_request.number) - os.system("echo ::set-output name=pr_number::%d" % pull_request.number) + os.system(f"echo ::set-env name=PULL_REQUEST_NUMBER::{pull_request.number}") + os.system(f"echo ::set-output name=pr_number::{pull_request.number}") # Set labels, assignees and milestone if labels is not None: - print("Applying labels '%s'" % labels) + print(f"Applying labels '{labels}'") pull_request.as_issue().edit(labels=cs_string_to_list(labels)) if assignees is not None: - print("Applying assignees '%s'" % assignees) + print(f"Applying assignees '{assignees}'") pull_request.as_issue().edit(assignees=cs_string_to_list(assignees)) if milestone is not None: - print("Applying milestone '%s'" % milestone) + print(f"Applying milestone '{milestone}'") milestone = github_repo.get_milestone(int(milestone)) pull_request.as_issue().edit(milestone=milestone) # Set pull request reviewers if reviewers is not None: - print("Requesting reviewers '%s'" % reviewers) + print(f"Requesting reviewers '{reviewers}'") try: pull_request.create_review_request(reviewers=cs_string_to_list(reviewers)) except GithubException as e: - # Likely caused by "Review cannot be requested from pull request - # author." + # Likely caused by "Review cannot be requested from pull request author." if e.status == 422: - print("Requesting reviewers failed - %s" % e.data["message"]) + print("Request reviewers failed - {}".format(e.data["message"])) # Set pull request team reviewers if team_reviewers is not None: - print("Requesting team reviewers '%s'" % team_reviewers) + print(f"Requesting team reviewers '{team_reviewers}'") pull_request.create_review_request( team_reviewers=cs_string_to_list(team_reviewers) ) @@ -126,5 +120,7 @@ def create_or_update_pull_request( # Likely caused by "Project already has the associated issue." if e.status == 422: print( - "Create project card failed - %s" % e.data["errors"][0]["message"] + "Create project card failed - {}".format( + e.data["errors"][0]["message"] + ) ) diff --git a/src/create_or_update_pull_request.py b/src/create_or_update_pull_request.py index 52b3101..009be66 100644 --- a/src/create_or_update_pull_request.py +++ b/src/create_or_update_pull_request.py @@ -63,55 +63,49 @@ def create_or_update_pull_request( pull_request = github_repo.create_pull( title=title, body=body, base=base, head=branch ) - print( - "Created pull request #%d (%s => %s)" % (pull_request.number, branch, base) - ) + print(f"Created pull request #{pull_request.number} ({branch} => {base})") except GithubException as e: if e.status == 422: - # Format the branch name - head_branch = "%s:%s" % (github_repository.split("/")[0], branch) + # A pull request exists for this branch and base + head_branch = "{}:{}".format(github_repository.split("/")[0], branch) # Get the pull request pull_request = github_repo.get_pulls( state="open", base=base, head=head_branch )[0] - print( - "Updated pull request #%d (%s => %s)" - % (pull_request.number, branch, base) - ) + print(f"Updated pull request #{pull_request.number} ({branch} => {base})") else: print(str(e)) raise # Set the output variables - os.system("echo ::set-env name=PULL_REQUEST_NUMBER::%d" % pull_request.number) - os.system("echo ::set-output name=pr_number::%d" % pull_request.number) + os.system(f"echo ::set-env name=PULL_REQUEST_NUMBER::{pull_request.number}") + os.system(f"echo ::set-output name=pr_number::{pull_request.number}") # Set labels, assignees and milestone if labels is not None: - print("Applying labels '%s'" % labels) + print(f"Applying labels '{labels}'") pull_request.as_issue().edit(labels=cs_string_to_list(labels)) if assignees is not None: - print("Applying assignees '%s'" % assignees) + print(f"Applying assignees '{assignees}'") pull_request.as_issue().edit(assignees=cs_string_to_list(assignees)) if milestone is not None: - print("Applying milestone '%s'" % milestone) + print(f"Applying milestone '{milestone}'") milestone = github_repo.get_milestone(int(milestone)) pull_request.as_issue().edit(milestone=milestone) # Set pull request reviewers if reviewers is not None: - print("Requesting reviewers '%s'" % reviewers) + print(f"Requesting reviewers '{reviewers}'") try: pull_request.create_review_request(reviewers=cs_string_to_list(reviewers)) except GithubException as e: - # Likely caused by "Review cannot be requested from pull request - # author." + # Likely caused by "Review cannot be requested from pull request author." if e.status == 422: - print("Requesting reviewers failed - %s" % e.data["message"]) + print("Request reviewers failed - {}".format(e.data["message"])) # Set pull request team reviewers if team_reviewers is not None: - print("Requesting team reviewers '%s'" % team_reviewers) + print(f"Requesting team reviewers '{team_reviewers}'") pull_request.create_review_request( team_reviewers=cs_string_to_list(team_reviewers) ) @@ -126,5 +120,7 @@ def create_or_update_pull_request( # Likely caused by "Project already has the associated issue." if e.status == 422: print( - "Create project card failed - %s" % e.data["errors"][0]["message"] + "Create project card failed - {}".format( + e.data["errors"][0]["message"] + ) ) From 883f800b969ce7348b2d6074e01e7ad0be9f167f Mon Sep 17 00:00:00 2001 From: Peter Evans Date: Sat, 28 Dec 2019 17:10:28 +0900 Subject: [PATCH 05/30] Change missing project and column to an error --- dist/src/create_or_update_pull_request.py | 4 ++-- src/create_or_update_pull_request.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dist/src/create_or_update_pull_request.py b/dist/src/create_or_update_pull_request.py index 009be66..52d53ef 100644 --- a/dist/src/create_or_update_pull_request.py +++ b/dist/src/create_or_update_pull_request.py @@ -20,7 +20,7 @@ def create_project_card(github_repo, project_name, project_column_name, pull_req break if not project: - print("::warning::Project not found. Unable to create project card.") + print("::error::Project not found. Unable to create project card.") return # Locate the column by name @@ -31,7 +31,7 @@ def create_project_card(github_repo, project_name, project_column_name, pull_req break if not column: - print("::warning::Project column not found. Unable to create project card.") + print("::error::Project column not found. Unable to create project card.") return # Create a project card for the pull request diff --git a/src/create_or_update_pull_request.py b/src/create_or_update_pull_request.py index 009be66..52d53ef 100644 --- a/src/create_or_update_pull_request.py +++ b/src/create_or_update_pull_request.py @@ -20,7 +20,7 @@ def create_project_card(github_repo, project_name, project_column_name, pull_req break if not project: - print("::warning::Project not found. Unable to create project card.") + print("::error::Project not found. Unable to create project card.") return # Locate the column by name @@ -31,7 +31,7 @@ def create_project_card(github_repo, project_name, project_column_name, pull_req break if not column: - print("::warning::Project column not found. Unable to create project card.") + print("::error::Project column not found. Unable to create project card.") return # Create a project card for the pull request From 71f4fe31a8c15f5689b115de0e0a1129005c9f0f Mon Sep 17 00:00:00 2001 From: Peter Evans Date: Sat, 28 Dec 2019 17:31:19 +0900 Subject: [PATCH 06/30] Update integration tests --- dist/src/test_create_or_update_branch.py | 47 ++++++++++++------------ src/test_create_or_update_branch.py | 47 ++++++++++++------------ 2 files changed, 46 insertions(+), 48 deletions(-) diff --git a/dist/src/test_create_or_update_branch.py b/dist/src/test_create_or_update_branch.py index c6ff95b..666b7d5 100644 --- a/dist/src/test_create_or_update_branch.py +++ b/dist/src/test_create_or_update_branch.py @@ -8,12 +8,11 @@ import sys import time +# Set git environment author_name = "github-actions[bot]" author_email = "41898282+github-actions[bot]@users.noreply.github.com" committer_name = "GitHub" committer_email = "noreply@github.com" - -# Set git environment repo = Repo(os.getcwd()) repo.git.update_environment( GIT_AUTHOR_NAME=author_name, @@ -718,31 +717,31 @@ def coub_wbnb_changes_and_commits_on_base_and_working_base(): # pytest -v -s ~/git/create-pull-request/src -# test_coub_fetch_successful = coub_fetch_successful +test_coub_fetch_successful = coub_fetch_successful -# test_coub_no_changes_on_create = coub_no_changes_on_create -# test_coub_tracked_changes = coub_tracked_changes -# test_coub_untracked_changes = coub_untracked_changes -# test_coub_identical_changes = coub_identical_changes -# test_coub_commits_on_base = coub_commits_on_base +test_coub_no_changes_on_create = coub_no_changes_on_create +test_coub_tracked_changes = coub_tracked_changes +test_coub_untracked_changes = coub_untracked_changes +test_coub_identical_changes = coub_identical_changes +test_coub_commits_on_base = coub_commits_on_base -# test_coub_changes_no_diff = coub_changes_no_diff -# test_coub_commits_on_base_no_diff = coub_commits_on_base_no_diff +test_coub_changes_no_diff = coub_changes_no_diff +test_coub_commits_on_base_no_diff = coub_commits_on_base_no_diff -# test_coub_commits_on_working_base = coub_commits_on_working_base -# test_coub_changes_and_commits_on_working_base = coub_changes_and_commits_on_working_base -# test_coub_changes_and_commits_on_base_and_working_base = coub_changes_and_commits_on_base_and_working_base +test_coub_commits_on_working_base = coub_commits_on_working_base +test_coub_changes_and_commits_on_working_base = coub_changes_and_commits_on_working_base +test_coub_changes_and_commits_on_base_and_working_base = coub_changes_and_commits_on_base_and_working_base -# # WBNB -# test_coub_wbnb_no_changes_on_create = coub_wbnb_no_changes_on_create -# test_coub_wbnb_tracked_changes = coub_wbnb_tracked_changes -# test_coub_wbnb_untracked_changes = coub_wbnb_untracked_changes -# test_coub_wbnb_identical_changes = coub_wbnb_identical_changes -# test_coub_wbnb_commits_on_base = coub_wbnb_commits_on_base +# WBNB +test_coub_wbnb_no_changes_on_create = coub_wbnb_no_changes_on_create +test_coub_wbnb_tracked_changes = coub_wbnb_tracked_changes +test_coub_wbnb_untracked_changes = coub_wbnb_untracked_changes +test_coub_wbnb_identical_changes = coub_wbnb_identical_changes +test_coub_wbnb_commits_on_base = coub_wbnb_commits_on_base -# test_coub_wbnb_changes_no_diff = coub_wbnb_changes_no_diff -# test_coub_wbnb_commits_on_base_no_diff = coub_wbnb_commits_on_base_no_diff +test_coub_wbnb_changes_no_diff = coub_wbnb_changes_no_diff +test_coub_wbnb_commits_on_base_no_diff = coub_wbnb_commits_on_base_no_diff -# test_coub_wbnb_commits_on_working_base = coub_wbnb_commits_on_working_base -# test_coub_wbnb_changes_and_commits_on_working_base = coub_wbnb_changes_and_commits_on_working_base -# test_coub_wbnb_changes_and_commits_on_base_and_working_base = coub_wbnb_changes_and_commits_on_base_and_working_base +test_coub_wbnb_commits_on_working_base = coub_wbnb_commits_on_working_base +test_coub_wbnb_changes_and_commits_on_working_base = coub_wbnb_changes_and_commits_on_working_base +test_coub_wbnb_changes_and_commits_on_base_and_working_base = coub_wbnb_changes_and_commits_on_base_and_working_base diff --git a/src/test_create_or_update_branch.py b/src/test_create_or_update_branch.py index c6ff95b..666b7d5 100644 --- a/src/test_create_or_update_branch.py +++ b/src/test_create_or_update_branch.py @@ -8,12 +8,11 @@ import sys import time +# Set git environment author_name = "github-actions[bot]" author_email = "41898282+github-actions[bot]@users.noreply.github.com" committer_name = "GitHub" committer_email = "noreply@github.com" - -# Set git environment repo = Repo(os.getcwd()) repo.git.update_environment( GIT_AUTHOR_NAME=author_name, @@ -718,31 +717,31 @@ def coub_wbnb_changes_and_commits_on_base_and_working_base(): # pytest -v -s ~/git/create-pull-request/src -# test_coub_fetch_successful = coub_fetch_successful +test_coub_fetch_successful = coub_fetch_successful -# test_coub_no_changes_on_create = coub_no_changes_on_create -# test_coub_tracked_changes = coub_tracked_changes -# test_coub_untracked_changes = coub_untracked_changes -# test_coub_identical_changes = coub_identical_changes -# test_coub_commits_on_base = coub_commits_on_base +test_coub_no_changes_on_create = coub_no_changes_on_create +test_coub_tracked_changes = coub_tracked_changes +test_coub_untracked_changes = coub_untracked_changes +test_coub_identical_changes = coub_identical_changes +test_coub_commits_on_base = coub_commits_on_base -# test_coub_changes_no_diff = coub_changes_no_diff -# test_coub_commits_on_base_no_diff = coub_commits_on_base_no_diff +test_coub_changes_no_diff = coub_changes_no_diff +test_coub_commits_on_base_no_diff = coub_commits_on_base_no_diff -# test_coub_commits_on_working_base = coub_commits_on_working_base -# test_coub_changes_and_commits_on_working_base = coub_changes_and_commits_on_working_base -# test_coub_changes_and_commits_on_base_and_working_base = coub_changes_and_commits_on_base_and_working_base +test_coub_commits_on_working_base = coub_commits_on_working_base +test_coub_changes_and_commits_on_working_base = coub_changes_and_commits_on_working_base +test_coub_changes_and_commits_on_base_and_working_base = coub_changes_and_commits_on_base_and_working_base -# # WBNB -# test_coub_wbnb_no_changes_on_create = coub_wbnb_no_changes_on_create -# test_coub_wbnb_tracked_changes = coub_wbnb_tracked_changes -# test_coub_wbnb_untracked_changes = coub_wbnb_untracked_changes -# test_coub_wbnb_identical_changes = coub_wbnb_identical_changes -# test_coub_wbnb_commits_on_base = coub_wbnb_commits_on_base +# WBNB +test_coub_wbnb_no_changes_on_create = coub_wbnb_no_changes_on_create +test_coub_wbnb_tracked_changes = coub_wbnb_tracked_changes +test_coub_wbnb_untracked_changes = coub_wbnb_untracked_changes +test_coub_wbnb_identical_changes = coub_wbnb_identical_changes +test_coub_wbnb_commits_on_base = coub_wbnb_commits_on_base -# test_coub_wbnb_changes_no_diff = coub_wbnb_changes_no_diff -# test_coub_wbnb_commits_on_base_no_diff = coub_wbnb_commits_on_base_no_diff +test_coub_wbnb_changes_no_diff = coub_wbnb_changes_no_diff +test_coub_wbnb_commits_on_base_no_diff = coub_wbnb_commits_on_base_no_diff -# test_coub_wbnb_commits_on_working_base = coub_wbnb_commits_on_working_base -# test_coub_wbnb_changes_and_commits_on_working_base = coub_wbnb_changes_and_commits_on_working_base -# test_coub_wbnb_changes_and_commits_on_base_and_working_base = coub_wbnb_changes_and_commits_on_base_and_working_base +test_coub_wbnb_commits_on_working_base = coub_wbnb_commits_on_working_base +test_coub_wbnb_changes_and_commits_on_working_base = coub_wbnb_changes_and_commits_on_working_base +test_coub_wbnb_changes_and_commits_on_base_and_working_base = coub_wbnb_changes_and_commits_on_base_and_working_base From 484de7fc89afbab850ffaba1c8cc44a81ed6bfa8 Mon Sep 17 00:00:00 2001 From: Peter Evans Date: Sat, 28 Dec 2019 18:34:32 +0900 Subject: [PATCH 07/30] Delete the branch if there is no diff with the base --- dist/src/create_pull_request.py | 7 +++++++ src/create_pull_request.py | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/dist/src/create_pull_request.py b/dist/src/create_pull_request.py index b0723e9..59fd890 100755 --- a/dist/src/create_pull_request.py +++ b/dist/src/create_pull_request.py @@ -170,6 +170,13 @@ if result["action"] in ["created", "updated"]: # Set the base. It would have been 'None' if not specified as an input base = result["base"] + # If there is no longer a diff with the base delete the branch and exit + if not result["diff"]: + print(f"Branch '{branch}' no longer differs from base branch '{base}'") + print(f"Closing pull request and deleting branch '{branch}'") + repo.git.push("--delete", "--force", repo_url, f"refs/heads/{branch}") + sys.exit() + # TODO Figure out what to do when there is no diff with the base anymore # if not result["diff"]: diff --git a/src/create_pull_request.py b/src/create_pull_request.py index b0723e9..59fd890 100755 --- a/src/create_pull_request.py +++ b/src/create_pull_request.py @@ -170,6 +170,13 @@ if result["action"] in ["created", "updated"]: # Set the base. It would have been 'None' if not specified as an input base = result["base"] + # If there is no longer a diff with the base delete the branch and exit + if not result["diff"]: + print(f"Branch '{branch}' no longer differs from base branch '{base}'") + print(f"Closing pull request and deleting branch '{branch}'") + repo.git.push("--delete", "--force", repo_url, f"refs/heads/{branch}") + sys.exit() + # TODO Figure out what to do when there is no diff with the base anymore # if not result["diff"]: From 01aec28fd9a12bd79d9dd391f58b80441526939c Mon Sep 17 00:00:00 2001 From: Peter Evans Date: Sat, 28 Dec 2019 19:05:35 +0900 Subject: [PATCH 08/30] Remove todo comment --- dist/src/create_pull_request.py | 3 --- src/create_pull_request.py | 3 --- 2 files changed, 6 deletions(-) diff --git a/dist/src/create_pull_request.py b/dist/src/create_pull_request.py index 59fd890..9a7f8ba 100755 --- a/dist/src/create_pull_request.py +++ b/dist/src/create_pull_request.py @@ -177,9 +177,6 @@ if result["action"] in ["created", "updated"]: repo.git.push("--delete", "--force", repo_url, f"refs/heads/{branch}") sys.exit() - # TODO Figure out what to do when there is no diff with the base anymore - # if not result["diff"]: - # Fetch optional environment variables with default values title = os.getenv("CPR_TITLE", "Auto-generated by create-pull-request action") body = os.getenv( diff --git a/src/create_pull_request.py b/src/create_pull_request.py index 59fd890..9a7f8ba 100755 --- a/src/create_pull_request.py +++ b/src/create_pull_request.py @@ -177,9 +177,6 @@ if result["action"] in ["created", "updated"]: repo.git.push("--delete", "--force", repo_url, f"refs/heads/{branch}") sys.exit() - # TODO Figure out what to do when there is no diff with the base anymore - # if not result["diff"]: - # Fetch optional environment variables with default values title = os.getenv("CPR_TITLE", "Auto-generated by create-pull-request action") body = os.getenv( From c208033c6a4388d9906e37e3f899ec9969777c81 Mon Sep 17 00:00:00 2001 From: Peter Evans Date: Sun, 29 Dec 2019 14:22:23 +0900 Subject: [PATCH 09/30] Remove v1 action code --- dist/src/create-pull-request.py | 352 -------------------------------- src/create-pull-request.py | 352 -------------------------------- 2 files changed, 704 deletions(-) delete mode 100755 dist/src/create-pull-request.py delete mode 100755 src/create-pull-request.py diff --git a/dist/src/create-pull-request.py b/dist/src/create-pull-request.py deleted file mode 100755 index 4999198..0000000 --- a/dist/src/create-pull-request.py +++ /dev/null @@ -1,352 +0,0 @@ -#!/usr/bin/env python3 -""" Create Pull Request """ -import json -import os -import random -import string -import sys -import time -from git import Repo -from github import Github, GithubException - - -def get_github_event(github_event_path): - with open(github_event_path) as f: - github_event = json.load(f) - if bool(os.environ.get("DEBUG_EVENT")): - print(os.environ["GITHUB_EVENT_NAME"]) - print(json.dumps(github_event, sort_keys=True, indent=2)) - return github_event - - -def get_head_short_sha1(repo): - return repo.git.rev_parse("--short", "HEAD") - - -def get_random_suffix(size=7, chars=string.ascii_lowercase + string.digits): - return "".join(random.choice(chars) for _ in range(size)) - - -def remote_branch_exists(repo, branch): - for ref in repo.remotes.origin.refs: - if ref.name == ("origin/%s" % branch): - return True - return False - - -def get_author_default(event_name, event_data): - if event_name == "push": - email = "{head_commit[author][email]}".format(**event_data) - name = "{head_commit[author][name]}".format(**event_data) - else: - email = os.environ["GITHUB_ACTOR"] + "@users.noreply.github.com" - name = os.environ["GITHUB_ACTOR"] - return email, name - - -def get_repo_url(token, github_repository): - return "https://x-access-token:%s@github.com/%s" % (token, github_repository) - - -def checkout_branch(git, remote_exists, branch): - if remote_exists: - print("Checking out branch '%s'" % branch) - git.stash("--include-untracked") - git.checkout(branch) - try: - git.stash("pop") - except BaseException: - git.checkout("--theirs", ".") - git.reset() - else: - print("Creating new branch '%s'" % branch) - git.checkout("HEAD", b=branch) - - -def push_changes(git, token, github_repository, branch, commit_message): - git.add("-A") - git.commit(m=commit_message) - repo_url = get_repo_url(token, github_repository) - return git.push("-f", repo_url, f"HEAD:refs/heads/{branch}") - - -def cs_string_to_list(str): - # Split the comma separated string into a list - l = [i.strip() for i in str.split(",")] - # Remove empty strings - return list(filter(None, l)) - - -def create_project_card(github_repo, project_name, project_column_name, pull_request): - # Locate the project by name - project = None - for project_item in github_repo.get_projects("all"): - if project_item.name == project_name: - project = project_item - break - - if not project: - print("::warning::Project not found. Unable to create project card.") - return - - # Locate the column by name - column = None - for column_item in project.get_columns(): - if column_item.name == project_column_name: - column = column_item - break - - if not column: - print("::warning::Project column not found. Unable to create project card.") - return - - # Create a project card for the pull request - column.create_card(content_id=pull_request.id, content_type="PullRequest") - print( - "Added pull request #%d to project '%s' under column '%s'" - % (pull_request.number, project.name, column.name) - ) - - -def process_event(github_token, github_repository, repo, branch, base): - # Fetch optional environment variables with default values - commit_message = os.getenv( - "COMMIT_MESSAGE", "Auto-committed changes by create-pull-request action" - ) - title = os.getenv( - "PULL_REQUEST_TITLE", "Auto-generated by create-pull-request action" - ) - body = os.getenv( - "PULL_REQUEST_BODY", - "Auto-generated pull request by " - "[create-pull-request](https://github.com/peter-evans/create-pull-request) GitHub Action", - ) - # Fetch optional environment variables with no default values - pull_request_labels = os.environ.get("PULL_REQUEST_LABELS") - pull_request_assignees = os.environ.get("PULL_REQUEST_ASSIGNEES") - pull_request_milestone = os.environ.get("PULL_REQUEST_MILESTONE") - pull_request_reviewers = os.environ.get("PULL_REQUEST_REVIEWERS") - pull_request_team_reviewers = os.environ.get("PULL_REQUEST_TEAM_REVIEWERS") - project_name = os.environ.get("PROJECT_NAME") - project_column_name = os.environ.get("PROJECT_COLUMN_NAME") - - # Push the local changes to the remote branch - print("Pushing changes to 'origin/%s'" % branch) - push_result = push_changes( - repo.git, github_token, github_repository, branch, commit_message - ) - print(push_result) - - # Create the pull request - github_repo = Github(github_token).get_repo(github_repository) - try: - pull_request = github_repo.create_pull( - title=title, body=body, base=base, head=branch - ) - print( - "Created pull request #%d (%s => %s)" % (pull_request.number, branch, base) - ) - except GithubException as e: - if e.status == 422: - # Format the branch name - head_branch = "%s:%s" % (github_repository.split("/")[0], branch) - # Get the pull request - pull_request = github_repo.get_pulls( - state="open", base=base, head=head_branch - )[0] - print( - "Updated pull request #%d (%s => %s)" - % (pull_request.number, branch, base) - ) - else: - print(str(e)) - sys.exit(1) - - # Set the output variables - os.system("echo ::set-env name=PULL_REQUEST_NUMBER::%d" % pull_request.number) - os.system("echo ::set-output name=pr_number::%d" % pull_request.number) - - # Set labels, assignees and milestone - if pull_request_labels is not None: - print("Applying labels '%s'" % pull_request_labels) - pull_request.as_issue().edit(labels=cs_string_to_list(pull_request_labels)) - if pull_request_assignees is not None: - print("Applying assignees '%s'" % pull_request_assignees) - pull_request.as_issue().edit( - assignees=cs_string_to_list(pull_request_assignees) - ) - if pull_request_milestone is not None: - print("Applying milestone '%s'" % pull_request_milestone) - milestone = github_repo.get_milestone(int(pull_request_milestone)) - pull_request.as_issue().edit(milestone=milestone) - - # Set pull request reviewers - if pull_request_reviewers is not None: - print("Requesting reviewers '%s'" % pull_request_reviewers) - try: - pull_request.create_review_request( - reviewers=cs_string_to_list(pull_request_reviewers) - ) - except GithubException as e: - # Likely caused by "Review cannot be requested from pull request - # author." - if e.status == 422: - print("Requesting reviewers failed - %s" % e.data["message"]) - - # Set pull request team reviewers - if pull_request_team_reviewers is not None: - print("Requesting team reviewers '%s'" % pull_request_team_reviewers) - pull_request.create_review_request( - team_reviewers=cs_string_to_list(pull_request_team_reviewers) - ) - - # Create a project card for the pull request - if project_name is not None and project_column_name is not None: - try: - create_project_card( - github_repo, project_name, project_column_name, pull_request - ) - except GithubException as e: - # Likely caused by "Project already has the associated issue." - if e.status == 422: - print( - "Create project card failed - %s" % e.data["errors"][0]["message"] - ) - - -# Fetch environment variables -github_token = os.environ["GITHUB_TOKEN"] -github_repository = os.environ["GITHUB_REPOSITORY"] -github_ref = os.environ["GITHUB_REF"] -event_name = os.environ["GITHUB_EVENT_NAME"] -# Get the JSON event data -event_data = get_github_event(os.environ["GITHUB_EVENT_PATH"]) - -# Get the default for author email and name -author_email, author_name = get_author_default(event_name, event_data) -# Set author name and email overrides -author_name = os.getenv("COMMIT_AUTHOR_NAME", author_name) -author_email = os.getenv("COMMIT_AUTHOR_EMAIL", author_email) -# Set committer name and email overrides -committer_name = os.getenv("COMMITTER_NAME", author_name) -committer_email = os.getenv("COMMITTER_EMAIL", author_email) - -# Set the repo to the working directory -repo = Repo(os.getcwd()) -# Set git environment. This will not persist after the action completes. -print("Configuring git author as '%s <%s>'" % (author_name, author_email)) -print("Configuring git committer as '%s <%s>'" % (committer_name, committer_email)) -repo.git.update_environment( - GIT_AUTHOR_NAME=author_name, - GIT_AUTHOR_EMAIL=author_email, - GIT_COMMITTER_NAME=committer_name, - GIT_COMMITTER_EMAIL=committer_email, -) - -# Fetch/Set the branch name -branch_prefix = os.getenv("PULL_REQUEST_BRANCH", "create-pull-request/patch") -# Fetch an optional base branch override -base_override = os.environ.get("PULL_REQUEST_BASE") - -# Set the base branch -if base_override is not None: - base = base_override - print("Overriding the base with branch '%s'" % base) - checkout_branch(repo.git, True, base) -elif github_ref.startswith("refs/pull/"): - # Check the PR is not raised from a fork of the repository - head_repo = "{pull_request[head][repo][full_name]}".format(**event_data) - if head_repo != github_repository: - print( - "::warning::Pull request was raised from a fork of the repository. " - + "Limitations on forked repositories have been imposed by GitHub Actions. " - + "Unable to continue. Exiting." - ) - sys.exit() - # Switch to the merging branch instead of the merge commit - base = os.environ["GITHUB_HEAD_REF"] - print( - "Removing the merge commit by switching to the pull request head branch '%s'" - % base - ) - checkout_branch(repo.git, True, base) -elif github_ref.startswith("refs/heads/"): - base = github_ref[11:] - print("Currently checked out base assumed to be branch '%s'" % base) -else: - print( - f"::warning::Currently checked out ref '{github_ref}' is not a valid base for a pull request. " - + "Unable to continue. Exiting." - ) - sys.exit() - -# Skip if the current branch is a PR branch created by this action. -# This may occur when using a PAT instead of GITHUB_TOKEN because -# a PAT allows workflow actions to trigger further events. -if base.startswith(branch_prefix): - print("Branch '%s' was created by this action. Skipping." % base) - sys.exit() - -# Fetch an optional environment variable to determine the branch suffix -branch_suffix = os.getenv("BRANCH_SUFFIX", "short-commit-hash") -if branch_suffix == "short-commit-hash": - # Suffix with the short SHA1 hash - branch = "%s-%s" % (branch_prefix, get_head_short_sha1(repo)) -elif branch_suffix == "timestamp": - # Suffix with the current timestamp - branch = "%s-%s" % (branch_prefix, int(time.time())) -elif branch_suffix == "random": - # Suffix with the current timestamp - branch = "%s-%s" % (branch_prefix, get_random_suffix()) -elif branch_suffix == "none": - # Fixed branch name - branch = branch_prefix -else: - print("Branch suffix '%s' is not a valid value." % branch_suffix) - sys.exit(1) - -# Output head branch -print("Pull request branch to create/update set to '%s'" % branch) - -# Check if the determined head branch exists as a remote -remote_exists = remote_branch_exists(repo, branch) -if remote_exists: - print( - "Pull request branch '%s' already exists as remote branch 'origin/%s'" - % (branch, branch) - ) - if branch_suffix == "short-commit-hash": - # A remote branch already exists for the HEAD commit - print( - "Pull request branch '%s' already exists for this commit. Skipping." - % branch - ) - sys.exit() - elif branch_suffix in ["timestamp", "random"]: - # Generated branch name collision with an existing branch - print( - "Pull request branch '%s' collided with a branch of the same name. Please re-run." - % branch - ) - sys.exit(1) - -# Checkout branch -checkout_branch(repo.git, remote_exists, branch) - -# Check if there are changes to pull request -if remote_exists: - print( - "Checking for local working copy changes indicating a " - + "diff with existing pull request branch 'origin/%s'" % branch - ) -else: - print( - "Checking for local working copy changes indicating a " - + "diff with base 'origin/%s'" % base - ) - -if repo.is_dirty() or len(repo.untracked_files) > 0: - print("Modified or untracked files detected.") - process_event(github_token, github_repository, repo, branch, base) -else: - print("No modified or untracked files detected. Skipping.") diff --git a/src/create-pull-request.py b/src/create-pull-request.py deleted file mode 100755 index 4999198..0000000 --- a/src/create-pull-request.py +++ /dev/null @@ -1,352 +0,0 @@ -#!/usr/bin/env python3 -""" Create Pull Request """ -import json -import os -import random -import string -import sys -import time -from git import Repo -from github import Github, GithubException - - -def get_github_event(github_event_path): - with open(github_event_path) as f: - github_event = json.load(f) - if bool(os.environ.get("DEBUG_EVENT")): - print(os.environ["GITHUB_EVENT_NAME"]) - print(json.dumps(github_event, sort_keys=True, indent=2)) - return github_event - - -def get_head_short_sha1(repo): - return repo.git.rev_parse("--short", "HEAD") - - -def get_random_suffix(size=7, chars=string.ascii_lowercase + string.digits): - return "".join(random.choice(chars) for _ in range(size)) - - -def remote_branch_exists(repo, branch): - for ref in repo.remotes.origin.refs: - if ref.name == ("origin/%s" % branch): - return True - return False - - -def get_author_default(event_name, event_data): - if event_name == "push": - email = "{head_commit[author][email]}".format(**event_data) - name = "{head_commit[author][name]}".format(**event_data) - else: - email = os.environ["GITHUB_ACTOR"] + "@users.noreply.github.com" - name = os.environ["GITHUB_ACTOR"] - return email, name - - -def get_repo_url(token, github_repository): - return "https://x-access-token:%s@github.com/%s" % (token, github_repository) - - -def checkout_branch(git, remote_exists, branch): - if remote_exists: - print("Checking out branch '%s'" % branch) - git.stash("--include-untracked") - git.checkout(branch) - try: - git.stash("pop") - except BaseException: - git.checkout("--theirs", ".") - git.reset() - else: - print("Creating new branch '%s'" % branch) - git.checkout("HEAD", b=branch) - - -def push_changes(git, token, github_repository, branch, commit_message): - git.add("-A") - git.commit(m=commit_message) - repo_url = get_repo_url(token, github_repository) - return git.push("-f", repo_url, f"HEAD:refs/heads/{branch}") - - -def cs_string_to_list(str): - # Split the comma separated string into a list - l = [i.strip() for i in str.split(",")] - # Remove empty strings - return list(filter(None, l)) - - -def create_project_card(github_repo, project_name, project_column_name, pull_request): - # Locate the project by name - project = None - for project_item in github_repo.get_projects("all"): - if project_item.name == project_name: - project = project_item - break - - if not project: - print("::warning::Project not found. Unable to create project card.") - return - - # Locate the column by name - column = None - for column_item in project.get_columns(): - if column_item.name == project_column_name: - column = column_item - break - - if not column: - print("::warning::Project column not found. Unable to create project card.") - return - - # Create a project card for the pull request - column.create_card(content_id=pull_request.id, content_type="PullRequest") - print( - "Added pull request #%d to project '%s' under column '%s'" - % (pull_request.number, project.name, column.name) - ) - - -def process_event(github_token, github_repository, repo, branch, base): - # Fetch optional environment variables with default values - commit_message = os.getenv( - "COMMIT_MESSAGE", "Auto-committed changes by create-pull-request action" - ) - title = os.getenv( - "PULL_REQUEST_TITLE", "Auto-generated by create-pull-request action" - ) - body = os.getenv( - "PULL_REQUEST_BODY", - "Auto-generated pull request by " - "[create-pull-request](https://github.com/peter-evans/create-pull-request) GitHub Action", - ) - # Fetch optional environment variables with no default values - pull_request_labels = os.environ.get("PULL_REQUEST_LABELS") - pull_request_assignees = os.environ.get("PULL_REQUEST_ASSIGNEES") - pull_request_milestone = os.environ.get("PULL_REQUEST_MILESTONE") - pull_request_reviewers = os.environ.get("PULL_REQUEST_REVIEWERS") - pull_request_team_reviewers = os.environ.get("PULL_REQUEST_TEAM_REVIEWERS") - project_name = os.environ.get("PROJECT_NAME") - project_column_name = os.environ.get("PROJECT_COLUMN_NAME") - - # Push the local changes to the remote branch - print("Pushing changes to 'origin/%s'" % branch) - push_result = push_changes( - repo.git, github_token, github_repository, branch, commit_message - ) - print(push_result) - - # Create the pull request - github_repo = Github(github_token).get_repo(github_repository) - try: - pull_request = github_repo.create_pull( - title=title, body=body, base=base, head=branch - ) - print( - "Created pull request #%d (%s => %s)" % (pull_request.number, branch, base) - ) - except GithubException as e: - if e.status == 422: - # Format the branch name - head_branch = "%s:%s" % (github_repository.split("/")[0], branch) - # Get the pull request - pull_request = github_repo.get_pulls( - state="open", base=base, head=head_branch - )[0] - print( - "Updated pull request #%d (%s => %s)" - % (pull_request.number, branch, base) - ) - else: - print(str(e)) - sys.exit(1) - - # Set the output variables - os.system("echo ::set-env name=PULL_REQUEST_NUMBER::%d" % pull_request.number) - os.system("echo ::set-output name=pr_number::%d" % pull_request.number) - - # Set labels, assignees and milestone - if pull_request_labels is not None: - print("Applying labels '%s'" % pull_request_labels) - pull_request.as_issue().edit(labels=cs_string_to_list(pull_request_labels)) - if pull_request_assignees is not None: - print("Applying assignees '%s'" % pull_request_assignees) - pull_request.as_issue().edit( - assignees=cs_string_to_list(pull_request_assignees) - ) - if pull_request_milestone is not None: - print("Applying milestone '%s'" % pull_request_milestone) - milestone = github_repo.get_milestone(int(pull_request_milestone)) - pull_request.as_issue().edit(milestone=milestone) - - # Set pull request reviewers - if pull_request_reviewers is not None: - print("Requesting reviewers '%s'" % pull_request_reviewers) - try: - pull_request.create_review_request( - reviewers=cs_string_to_list(pull_request_reviewers) - ) - except GithubException as e: - # Likely caused by "Review cannot be requested from pull request - # author." - if e.status == 422: - print("Requesting reviewers failed - %s" % e.data["message"]) - - # Set pull request team reviewers - if pull_request_team_reviewers is not None: - print("Requesting team reviewers '%s'" % pull_request_team_reviewers) - pull_request.create_review_request( - team_reviewers=cs_string_to_list(pull_request_team_reviewers) - ) - - # Create a project card for the pull request - if project_name is not None and project_column_name is not None: - try: - create_project_card( - github_repo, project_name, project_column_name, pull_request - ) - except GithubException as e: - # Likely caused by "Project already has the associated issue." - if e.status == 422: - print( - "Create project card failed - %s" % e.data["errors"][0]["message"] - ) - - -# Fetch environment variables -github_token = os.environ["GITHUB_TOKEN"] -github_repository = os.environ["GITHUB_REPOSITORY"] -github_ref = os.environ["GITHUB_REF"] -event_name = os.environ["GITHUB_EVENT_NAME"] -# Get the JSON event data -event_data = get_github_event(os.environ["GITHUB_EVENT_PATH"]) - -# Get the default for author email and name -author_email, author_name = get_author_default(event_name, event_data) -# Set author name and email overrides -author_name = os.getenv("COMMIT_AUTHOR_NAME", author_name) -author_email = os.getenv("COMMIT_AUTHOR_EMAIL", author_email) -# Set committer name and email overrides -committer_name = os.getenv("COMMITTER_NAME", author_name) -committer_email = os.getenv("COMMITTER_EMAIL", author_email) - -# Set the repo to the working directory -repo = Repo(os.getcwd()) -# Set git environment. This will not persist after the action completes. -print("Configuring git author as '%s <%s>'" % (author_name, author_email)) -print("Configuring git committer as '%s <%s>'" % (committer_name, committer_email)) -repo.git.update_environment( - GIT_AUTHOR_NAME=author_name, - GIT_AUTHOR_EMAIL=author_email, - GIT_COMMITTER_NAME=committer_name, - GIT_COMMITTER_EMAIL=committer_email, -) - -# Fetch/Set the branch name -branch_prefix = os.getenv("PULL_REQUEST_BRANCH", "create-pull-request/patch") -# Fetch an optional base branch override -base_override = os.environ.get("PULL_REQUEST_BASE") - -# Set the base branch -if base_override is not None: - base = base_override - print("Overriding the base with branch '%s'" % base) - checkout_branch(repo.git, True, base) -elif github_ref.startswith("refs/pull/"): - # Check the PR is not raised from a fork of the repository - head_repo = "{pull_request[head][repo][full_name]}".format(**event_data) - if head_repo != github_repository: - print( - "::warning::Pull request was raised from a fork of the repository. " - + "Limitations on forked repositories have been imposed by GitHub Actions. " - + "Unable to continue. Exiting." - ) - sys.exit() - # Switch to the merging branch instead of the merge commit - base = os.environ["GITHUB_HEAD_REF"] - print( - "Removing the merge commit by switching to the pull request head branch '%s'" - % base - ) - checkout_branch(repo.git, True, base) -elif github_ref.startswith("refs/heads/"): - base = github_ref[11:] - print("Currently checked out base assumed to be branch '%s'" % base) -else: - print( - f"::warning::Currently checked out ref '{github_ref}' is not a valid base for a pull request. " - + "Unable to continue. Exiting." - ) - sys.exit() - -# Skip if the current branch is a PR branch created by this action. -# This may occur when using a PAT instead of GITHUB_TOKEN because -# a PAT allows workflow actions to trigger further events. -if base.startswith(branch_prefix): - print("Branch '%s' was created by this action. Skipping." % base) - sys.exit() - -# Fetch an optional environment variable to determine the branch suffix -branch_suffix = os.getenv("BRANCH_SUFFIX", "short-commit-hash") -if branch_suffix == "short-commit-hash": - # Suffix with the short SHA1 hash - branch = "%s-%s" % (branch_prefix, get_head_short_sha1(repo)) -elif branch_suffix == "timestamp": - # Suffix with the current timestamp - branch = "%s-%s" % (branch_prefix, int(time.time())) -elif branch_suffix == "random": - # Suffix with the current timestamp - branch = "%s-%s" % (branch_prefix, get_random_suffix()) -elif branch_suffix == "none": - # Fixed branch name - branch = branch_prefix -else: - print("Branch suffix '%s' is not a valid value." % branch_suffix) - sys.exit(1) - -# Output head branch -print("Pull request branch to create/update set to '%s'" % branch) - -# Check if the determined head branch exists as a remote -remote_exists = remote_branch_exists(repo, branch) -if remote_exists: - print( - "Pull request branch '%s' already exists as remote branch 'origin/%s'" - % (branch, branch) - ) - if branch_suffix == "short-commit-hash": - # A remote branch already exists for the HEAD commit - print( - "Pull request branch '%s' already exists for this commit. Skipping." - % branch - ) - sys.exit() - elif branch_suffix in ["timestamp", "random"]: - # Generated branch name collision with an existing branch - print( - "Pull request branch '%s' collided with a branch of the same name. Please re-run." - % branch - ) - sys.exit(1) - -# Checkout branch -checkout_branch(repo.git, remote_exists, branch) - -# Check if there are changes to pull request -if remote_exists: - print( - "Checking for local working copy changes indicating a " - + "diff with existing pull request branch 'origin/%s'" % branch - ) -else: - print( - "Checking for local working copy changes indicating a " - + "diff with base 'origin/%s'" % base - ) - -if repo.is_dirty() or len(repo.untracked_files) > 0: - print("Modified or untracked files detected.") - process_event(github_token, github_repository, repo, branch, base) -else: - print("No modified or untracked files detected. Skipping.") From 9bdc8b271f80bfa09192d215ebfadd6689768b3f Mon Sep 17 00:00:00 2001 From: Peter Evans Date: Sun, 29 Dec 2019 14:22:44 +0900 Subject: [PATCH 10/30] Bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4301388..67d4b7f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "create-pull-request", - "version": "1.9.1", + "version": "2.0.0", "description": "Creates a pull request for changes to your repository in the actions workspace", "main": "index.js", "scripts": { From bdadb3b4a077507bac25214a1d169185cc44e746 Mon Sep 17 00:00:00 2001 From: Peter Evans Date: Sun, 29 Dec 2019 14:40:49 +0900 Subject: [PATCH 11/30] Update PyGithub to v1.45 --- dist/src/requirements.txt | 2 +- src/requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dist/src/requirements.txt b/dist/src/requirements.txt index dfc2aaf..35a4b5c 100644 --- a/dist/src/requirements.txt +++ b/dist/src/requirements.txt @@ -1,2 +1,2 @@ GitPython==3.0.5 -PyGithub==1.44.1 +PyGithub==1.45 diff --git a/src/requirements.txt b/src/requirements.txt index dfc2aaf..35a4b5c 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -1,2 +1,2 @@ GitPython==3.0.5 -PyGithub==1.44.1 +PyGithub==1.45 From b11cb71e96474f26a27234e4f652817234c573d5 Mon Sep 17 00:00:00 2001 From: Peter Evans Date: Sun, 29 Dec 2019 15:14:33 +0900 Subject: [PATCH 12/30] Update input defaults --- dist/src/create_pull_request.py | 21 +++++++++++---------- dist/src/test_create_or_update_branch.py | 14 ++++++++++---- src/create_pull_request.py | 21 +++++++++++---------- src/test_create_or_update_branch.py | 14 ++++++++++---- 4 files changed, 42 insertions(+), 28 deletions(-) diff --git a/dist/src/create_pull_request.py b/dist/src/create_pull_request.py index 9a7f8ba..e5a02e4 100755 --- a/dist/src/create_pull_request.py +++ b/dist/src/create_pull_request.py @@ -15,6 +15,13 @@ DEFAULT_COMMITTER = "GitHub " DEFAULT_AUTHOR = ( "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>" ) +DEFAULT_COMMIT_MESSAGE = "[create-pull-request] automated change" +DEFAULT_TITLE = "Changes by create-pull-request action" +DEFAULT_BODY = ( + "Automated changes by " + + "[create-pull-request](https://github.com/peter-evans/create-pull-request) GitHub action" +) +DEFAULT_BRANCH = "create-pull-request/patch" def get_git_config_value(repo, name): @@ -89,10 +96,8 @@ def set_committer_author(repo, committer, author): github_token = os.environ["GITHUB_TOKEN"] github_repository = os.environ["GITHUB_REPOSITORY"] # Get environment variables with defaults -branch = os.getenv("CPR_BRANCH", "create-pull-request/patch") -commit_message = os.getenv( - "CPR_COMMIT_MESSAGE", "Changes by create-pull-request action" -) +branch = os.getenv("CPR_BRANCH", DEFAULT_BRANCH) +commit_message = os.getenv("CPR_COMMIT_MESSAGE", DEFAULT_COMMIT_MESSAGE) # Get environment variables with a default of 'None' committer = os.environ.get("CPR_COMMITTER") author = os.environ.get("CPR_AUTHOR") @@ -178,12 +183,8 @@ if result["action"] in ["created", "updated"]: sys.exit() # Fetch optional environment variables with default values - title = os.getenv("CPR_TITLE", "Auto-generated by create-pull-request action") - body = os.getenv( - "CPR_BODY", - "Auto-generated pull request by " - "[create-pull-request](https://github.com/peter-evans/create-pull-request) GitHub Action", - ) + title = os.getenv("CPR_TITLE", DEFAULT_TITLE) + body = os.getenv("CPR_BODY", DEFAULT_BODY) # Create or update the pull request coupr.create_or_update_pull_request( diff --git a/dist/src/test_create_or_update_branch.py b/dist/src/test_create_or_update_branch.py index 666b7d5..2e2209b 100644 --- a/dist/src/test_create_or_update_branch.py +++ b/dist/src/test_create_or_update_branch.py @@ -30,7 +30,7 @@ DEFAULT_BRANCH = "tests/master" NOT_BASE_BRANCH = "tests/branch-that-is-not-the-base" NOT_EXIST_BRANCH = "tests/branch-that-does-not-exist" -COMMIT_MESSAGE = "Changes by create-pull-request action" +COMMIT_MESSAGE = "[create-pull-request] automated change" BRANCH = "tests/create-pull-request/patch" BASE = DEFAULT_BRANCH @@ -730,7 +730,9 @@ test_coub_commits_on_base_no_diff = coub_commits_on_base_no_diff test_coub_commits_on_working_base = coub_commits_on_working_base test_coub_changes_and_commits_on_working_base = coub_changes_and_commits_on_working_base -test_coub_changes_and_commits_on_base_and_working_base = coub_changes_and_commits_on_base_and_working_base +test_coub_changes_and_commits_on_base_and_working_base = ( + coub_changes_and_commits_on_base_and_working_base +) # WBNB test_coub_wbnb_no_changes_on_create = coub_wbnb_no_changes_on_create @@ -743,5 +745,9 @@ test_coub_wbnb_changes_no_diff = coub_wbnb_changes_no_diff test_coub_wbnb_commits_on_base_no_diff = coub_wbnb_commits_on_base_no_diff test_coub_wbnb_commits_on_working_base = coub_wbnb_commits_on_working_base -test_coub_wbnb_changes_and_commits_on_working_base = coub_wbnb_changes_and_commits_on_working_base -test_coub_wbnb_changes_and_commits_on_base_and_working_base = coub_wbnb_changes_and_commits_on_base_and_working_base +test_coub_wbnb_changes_and_commits_on_working_base = ( + coub_wbnb_changes_and_commits_on_working_base +) +test_coub_wbnb_changes_and_commits_on_base_and_working_base = ( + coub_wbnb_changes_and_commits_on_base_and_working_base +) diff --git a/src/create_pull_request.py b/src/create_pull_request.py index 9a7f8ba..e5a02e4 100755 --- a/src/create_pull_request.py +++ b/src/create_pull_request.py @@ -15,6 +15,13 @@ DEFAULT_COMMITTER = "GitHub " DEFAULT_AUTHOR = ( "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>" ) +DEFAULT_COMMIT_MESSAGE = "[create-pull-request] automated change" +DEFAULT_TITLE = "Changes by create-pull-request action" +DEFAULT_BODY = ( + "Automated changes by " + + "[create-pull-request](https://github.com/peter-evans/create-pull-request) GitHub action" +) +DEFAULT_BRANCH = "create-pull-request/patch" def get_git_config_value(repo, name): @@ -89,10 +96,8 @@ def set_committer_author(repo, committer, author): github_token = os.environ["GITHUB_TOKEN"] github_repository = os.environ["GITHUB_REPOSITORY"] # Get environment variables with defaults -branch = os.getenv("CPR_BRANCH", "create-pull-request/patch") -commit_message = os.getenv( - "CPR_COMMIT_MESSAGE", "Changes by create-pull-request action" -) +branch = os.getenv("CPR_BRANCH", DEFAULT_BRANCH) +commit_message = os.getenv("CPR_COMMIT_MESSAGE", DEFAULT_COMMIT_MESSAGE) # Get environment variables with a default of 'None' committer = os.environ.get("CPR_COMMITTER") author = os.environ.get("CPR_AUTHOR") @@ -178,12 +183,8 @@ if result["action"] in ["created", "updated"]: sys.exit() # Fetch optional environment variables with default values - title = os.getenv("CPR_TITLE", "Auto-generated by create-pull-request action") - body = os.getenv( - "CPR_BODY", - "Auto-generated pull request by " - "[create-pull-request](https://github.com/peter-evans/create-pull-request) GitHub Action", - ) + title = os.getenv("CPR_TITLE", DEFAULT_TITLE) + body = os.getenv("CPR_BODY", DEFAULT_BODY) # Create or update the pull request coupr.create_or_update_pull_request( diff --git a/src/test_create_or_update_branch.py b/src/test_create_or_update_branch.py index 666b7d5..2e2209b 100644 --- a/src/test_create_or_update_branch.py +++ b/src/test_create_or_update_branch.py @@ -30,7 +30,7 @@ DEFAULT_BRANCH = "tests/master" NOT_BASE_BRANCH = "tests/branch-that-is-not-the-base" NOT_EXIST_BRANCH = "tests/branch-that-does-not-exist" -COMMIT_MESSAGE = "Changes by create-pull-request action" +COMMIT_MESSAGE = "[create-pull-request] automated change" BRANCH = "tests/create-pull-request/patch" BASE = DEFAULT_BRANCH @@ -730,7 +730,9 @@ test_coub_commits_on_base_no_diff = coub_commits_on_base_no_diff test_coub_commits_on_working_base = coub_commits_on_working_base test_coub_changes_and_commits_on_working_base = coub_changes_and_commits_on_working_base -test_coub_changes_and_commits_on_base_and_working_base = coub_changes_and_commits_on_base_and_working_base +test_coub_changes_and_commits_on_base_and_working_base = ( + coub_changes_and_commits_on_base_and_working_base +) # WBNB test_coub_wbnb_no_changes_on_create = coub_wbnb_no_changes_on_create @@ -743,5 +745,9 @@ test_coub_wbnb_changes_no_diff = coub_wbnb_changes_no_diff test_coub_wbnb_commits_on_base_no_diff = coub_wbnb_commits_on_base_no_diff test_coub_wbnb_commits_on_working_base = coub_wbnb_commits_on_working_base -test_coub_wbnb_changes_and_commits_on_working_base = coub_wbnb_changes_and_commits_on_working_base -test_coub_wbnb_changes_and_commits_on_base_and_working_base = coub_wbnb_changes_and_commits_on_base_and_working_base +test_coub_wbnb_changes_and_commits_on_working_base = ( + coub_wbnb_changes_and_commits_on_working_base +) +test_coub_wbnb_changes_and_commits_on_base_and_working_base = ( + coub_wbnb_changes_and_commits_on_base_and_working_base +) From 673922cfd29841f62607a31b4fa4ba73b5c4f68a Mon Sep 17 00:00:00 2001 From: Peter Evans Date: Sun, 29 Dec 2019 15:50:32 +0900 Subject: [PATCH 13/30] Update README --- README.md | 68 +++++++++++++++++++++++++++---------------------------- 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 79274b3..10d9700 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,12 @@ The changes will be automatically committed to a new branch and a pull request c Create Pull Request action will: -1. Check for repository changes in the Actions workspace. This includes untracked (new) files as well as modified files. +1. Check for repository changes in the Actions workspace. This includes: + - untracked (new) files + - tracked (modified) files + - commits made during the workflow that have not been pushed 2. Commit all changes to a new branch, or update an existing pull request branch. -3. Create a pull request to merge the new branch into the currently active branch executing the workflow. +3. Create a pull request to merge the new branch into the base—the branch checked out in the workflow. ## Usage @@ -19,12 +22,12 @@ See [examples](examples.md) for detailed use cases. ```yml - name: Create Pull Request - uses: peter-evans/create-pull-request@v1 + uses: peter-evans/create-pull-request@v2-beta with: token: ${{ secrets.GITHUB_TOKEN }} ``` -You can also pin to a [specific release](https://github.com/peter-evans/create-pull-request/releases) version in the format `@v1.x.x` +You can also pin to a [specific release](https://github.com/peter-evans/create-pull-request/releases) version in the format `@v2.x.x` **Note**: If you want pull requests created by this action to trigger an `on: pull_request` workflow then you must use a [Personal Access Token](https://help.github.com/en/articles/creating-a-personal-access-token-for-the-command-line) instead of the default `GITHUB_TOKEN`. See [this issue](https://github.com/peter-evans/create-pull-request/issues/48) for further details. @@ -35,13 +38,11 @@ These inputs are *all optional*. If not set, sensible default values will be use | Name | Description | Default | | --- | --- | --- | -| `commit-message` | The message to use when committing changes. | `Auto-committed changes by create-pull-request action` | -| `author-name` | The name of the commit author. | For `push` events, the HEAD commit author. Otherwise, , the GitHub user that initiated the event. | -| `author-email` | The email address of the commit author. | For `push` events, the HEAD commit author. Otherwise, @users.noreply.github.com, where `GITHUB_ACTOR` is the GitHub user that initiated the event. | -| `committer-name` | The name of the committer. | Defaults to match `author-name` | -| `committer-email` | The email address of the committer. | Defaults to match `author-email` | -| `title` | The title of the pull request. | `Auto-generated by create-pull-request action` | -| `body` | The body of the pull request. | `Auto-generated pull request by [create-pull-request](https://github.com/peter-evans/create-pull-request) GitHub Action` | +| `commit-message` | The message to use when committing changes. | `[create-pull-request] automated change` | +| `committer` | The committer name and email address in the format `Display Name `. | Defaults to the GitHub Actions bot user. See [Committer and author](#committer-and-author) for details. | +| `author` | The author name and email address in the format `Display Name `. | Defaults to the GitHub Actions bot user. See [Committer and author](#committer-and-author) for details. | +| `title` | The title of the pull request. | `Changes by create-pull-request action` | +| `body` | The body of the pull request. | `Automated changes by [create-pull-request](https://github.com/peter-evans/create-pull-request) GitHub action` | | `labels` | A comma separated list of labels. | | | `assignees` | A comma separated list of assignees (GitHub usernames). | | | `reviewers` | A comma separated list of reviewers (GitHub usernames) to request a review from. | | @@ -49,9 +50,9 @@ These inputs are *all optional*. If not set, sensible default values will be use | `milestone` | The number of the milestone to associate this pull request with. | | | `project` | The name of the project for which a card should be created. Requires `project-column`. | | | `project-column` | The name of the project column under which a card should be created. Requires `project`. | | -| `branch` | The branch name. See **Branch naming** below for details. | `create-pull-request/patch` | -| `base` | Sets the pull request base branch. | Defaults to the currently checked out branch, `GITHUB_REF`. For `pull_request` events, `GITHUB_HEAD_REF` | -| `branch-suffix` | The branch suffix type. Valid values are `short-commit-hash`, `timestamp`, `random` and `none`. See **Branch naming** below for details. | `short-commit-hash` | +| `branch` | The branch name. See [Branch naming](#branch-naming) for details. | `create-pull-request/patch` | +| `base` | Sets the pull request base branch. | Defaults to the branch checked out in the workflow. | +| `branch-suffix` | The branch suffix type. Valid values are `random`, `timestamp` and `short-commit-hash`. See [Branch naming](#branch-naming) for details. | | **Outputs** @@ -61,7 +62,7 @@ Note that in order to read the step output the action step must have an id. ```yml - name: Create Pull Request id: cpr - uses: peter-evans/create-pull-request@v1 + uses: peter-evans/create-pull-request@v2-beta with: token: ${{ secrets.GITHUB_TOKEN }} - name: Check outputs @@ -72,40 +73,38 @@ Note that in order to read the step output the action step must have an id. ### Branch naming -For branch naming there are two strategies. Always create a new branch each time there are changes to be committed, OR, create a fixed-name pull request branch that will be updated with any new commits until it is merged or closed. +For branch naming there are two strategies. Create a fixed-name pull request branch that will be updated with new changes until it is merged or closed, OR, always create a new unique branch each time there are changes to be committed. -#### Strategy A - Always create a new pull request branch (default) +#### Strategy A - Create and update a pull request branch (default) + +This strategy is the default behaviour of the action. The input `branch` defaults to `create-pull-request/patch`. Changes will be committed to this branch and a pull request created. Any subsequent changes will be committed to the *same* branch and reflected in the open pull request. If the pull request is merged or closed a new one will be created. If subsequent changes cause the branch to no longer differ from the base the pull request will be automatically closed and the branch deleted. + +#### Strategy B - Always create a new pull request branch For this strategy there are three options to suffix the branch name. The branch name is defined by the input `branch` and defaults to `create-pull-request/patch`. The following options are values for `branch-suffix`. -- `short-commit-hash` (default) - Commits will be made to a branch suffixed with the short SHA1 commit hash. e.g. `create-pull-request/patch-fcdfb59`, `create-pull-request/patch-394710b` +- `random` - Commits will be made to a branch suffixed with a random alpha-numeric string. This option should be used if multiple pull requests will be created during the execution of a workflow. e.g. `create-pull-request/patch-6qj97jr`, `create-pull-request/patch-5jrjhvd` - `timestamp` - Commits will be made to a branch suffixed by a timestamp. e.g. `create-pull-request/patch-1569322532`, `create-pull-request/patch-1569322552` -- `random` - Commits will be made to a branch suffixed with a random alpha-numeric string. This option should be used if multiple pull requests will be created during the execution of a workflow. e.g. `create-pull-request/patch-6qj97jr`, `create-pull-request/patch-5jrjhvd` - -#### Strategy B - Create and update a pull request branch - -To use this strategy, set `branch-suffix` to the value `none`. The input `branch` defaults to `create-pull-request/patch`. Commits will be made to this branch and a pull request created. Any subsequent changes will be committed to the *same* branch and reflected in the open pull request. If the pull request is merged or closed a new one will be created. +- `short-commit-hash` - Commits will be made to a branch suffixed with the short SHA1 commit hash. e.g. `create-pull-request/patch-fcdfb59`, `create-pull-request/patch-394710b` ### Ignoring files If there are files or directories you want to ignore you can simply add them to a `.gitignore` file at the root of your repository. The action will respect this file. -### Commit as github-actions[bot] +### Committer and author -You can make commits that appear to be made by the GitHub Actions bot as follows. +If neither `committer` or `author` inputs are supplied the action will default to making commits that appear to be made by the GitHub Actions bot user. +In most cases, where the committer and author are the same, just the committer can be set. ```yml - name: Create Pull Request - uses: peter-evans/create-pull-request@v1 + uses: peter-evans/create-pull-request@v2-beta with: token: ${{ secrets.GITHUB_TOKEN }} - author-name: github-actions[bot] - author-email: 41898282+github-actions[bot]@users.noreply.github.com - committer-name: GitHub - committer-email: noreply@github.com + committer: Peter Evans ``` ## Reference Example @@ -121,17 +120,17 @@ jobs: createPullRequest: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - name: Create report file run: date +%s > report.txt - name: Create Pull Request id: cpr - uses: peter-evans/create-pull-request@v1 + uses: peter-evans/create-pull-request@v2-beta with: token: ${{ secrets.GITHUB_TOKEN }} commit-message: Add report file - author-email: peter-evans@users.noreply.github.com - author-name: Peter Evans + committer: Peter Evans + author: Peter Evans title: '[Example] Add report file' body: | New report @@ -146,7 +145,6 @@ jobs: project: Example Project project-column: To do branch: example-patches - branch-suffix: short-commit-hash - name: Check outputs run: | echo "Pull Request Number - ${{ env.PULL_REQUEST_NUMBER }}" From 8c86d0f83fc73cedcb14a9aeda40d61a8c1dce03 Mon Sep 17 00:00:00 2001 From: Peter Evans Date: Sun, 29 Dec 2019 16:05:24 +0900 Subject: [PATCH 14/30] Update workflows --- .github/workflows/create-pull-request-multi.yml | 7 +++---- .github/workflows/create-pull-request.yml | 6 ++---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/.github/workflows/create-pull-request-multi.yml b/.github/workflows/create-pull-request-multi.yml index c575863..89b2120 100644 --- a/.github/workflows/create-pull-request-multi.yml +++ b/.github/workflows/create-pull-request-multi.yml @@ -4,13 +4,13 @@ on: types: [create-pull-request-multi] jobs: createPullRequest: - name: Testing on ${{ matrix.platform }} + name: CPR on ${{ matrix.platform }} strategy: matrix: platform: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.platform }} steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - name: Create report file if: matrix.platform == 'ubuntu-latest' || matrix.platform == 'macos-latest' run: date +%s > report.txt @@ -23,8 +23,7 @@ jobs: with: token: ${{ secrets.GITHUB_TOKEN }} commit-message: Add report file - author-email: peter-evans@users.noreply.github.com - author-name: Peter Evans + committer: Peter Evans title: '[Example] Add report file' body: | New report diff --git a/.github/workflows/create-pull-request.yml b/.github/workflows/create-pull-request.yml index 42f2d1f..677f1be 100644 --- a/.github/workflows/create-pull-request.yml +++ b/.github/workflows/create-pull-request.yml @@ -6,7 +6,7 @@ jobs: createPullRequest: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - name: Create report file run: date +%s > report.txt - name: Create Pull Request @@ -15,8 +15,7 @@ jobs: with: token: ${{ secrets.GITHUB_TOKEN }} commit-message: Add report file - author-email: peter-evans@users.noreply.github.com - author-name: Peter Evans + committer: Peter Evans title: '[Example] Add report file' body: | New report @@ -31,7 +30,6 @@ jobs: project: Example Project project-column: To do branch: example-patches - branch-suffix: short-commit-hash - name: Check outputs run: | echo "Pull Request Number - ${{ env.PULL_REQUEST_NUMBER }}" From 3e4ab24cbb7fca795cbc315692acb9a06715c5bc Mon Sep 17 00:00:00 2001 From: Peter Evans Date: Sun, 29 Dec 2019 16:15:13 +0900 Subject: [PATCH 15/30] Update examples --- examples.md | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/examples.md b/examples.md index 080605f..10237af 100644 --- a/examples.md +++ b/examples.md @@ -31,7 +31,7 @@ jobs: update-deps: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - uses: actions/setup-node@v1 with: node-version: '10.x' @@ -42,14 +42,13 @@ jobs: ncu -u npm install - name: Create Pull Request - uses: peter-evans/create-pull-request@v1 + uses: peter-evans/create-pull-request@v2-beta with: token: ${{ secrets.GITHUB_TOKEN }} commit-message: update dependencies title: Automated Dependency Updates body: This is an auto-generated PR with dependency updates. branch: dep-updates - branch-suffix: none ``` ### Keep Go up to date @@ -68,7 +67,7 @@ jobs: fresh_go: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 with: ref: master - uses: jmhodges/ensure-latest-go@v1.0.2 @@ -76,13 +75,12 @@ jobs: - run: echo "##[set-output name=pr_title;]update to latest Go release ${{ steps.ensure_go.outputs.go_version}}" id: pr_title_maker - name: Create pull request - uses: peter-evans/create-pull-request@v1 + uses: peter-evans/create-pull-request@v2-beta with: token: ${{ secrets.GITHUB_TOKEN }} title: ${{ steps.pr_title_maker.outputs.pr_title }} body: Auto-generated pull request created by the GitHub Actions [create-pull-request](https://github.com/peter-evans/create-pull-request) and [ensure-latest-go](https://github.com/jmhodges/ensure-latest-go). commit-message: ${{ steps.pr_title_maker.outputs.pr_title }} - branch-suffix: none branch: ensure-latest-go/patch-${{ steps.ensure_go.outputs.go_version }} ``` @@ -100,7 +98,7 @@ jobs: updateSwagger: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - name: Get Latest Swagger UI Release id: swagger-ui run: | @@ -128,7 +126,7 @@ jobs: # Update current release echo ${{ steps.swagger-ui.outputs.release_tag }} > swagger-ui.version - name: Create Pull Request - uses: peter-evans/create-pull-request@v1 + uses: peter-evans/create-pull-request@v2-beta with: token: ${{ secrets.GITHUB_TOKEN }} commit-message: Update swagger-ui to ${{ steps.swagger-ui.outputs.release_tag }} @@ -142,7 +140,6 @@ jobs: [2]: https://github.com/peter-evans/create-pull-request labels: dependencies, automated pr branch: swagger-ui-updates - branch-suffix: none ``` ### Spider and download a website @@ -158,7 +155,7 @@ jobs: format: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - name: Download website run: | wget \ @@ -172,14 +169,13 @@ jobs: --domains quotes.toscrape.com \ http://quotes.toscrape.com/ - name: Create Pull Request - uses: peter-evans/create-pull-request@v1 + uses: peter-evans/create-pull-request@v2-beta with: token: ${{ secrets.GITHUB_TOKEN }} commit-message: update local website copy title: Automated Updates to Local Website Copy body: This is an auto-generated PR with website updates. branch: website-updates - branch-suffix: none ``` ## Use case: Create a pull request to update X by calling the GitHub API @@ -253,7 +249,7 @@ jobs: if: startsWith(github.head_ref, 'autopep8-patches') == false && github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - name: autopep8 id: autopep8 uses: peter-evans/autopep8@v1.1.0 @@ -264,7 +260,7 @@ jobs: run: echo ::set-output name=branch-name::"autopep8-patches/$GITHUB_HEAD_REF" - name: Create Pull Request if: steps.autopep8.outputs.exit-code == 2 - uses: peter-evans/create-pull-request@v1 + uses: peter-evans/create-pull-request@v2-beta with: token: ${{ secrets.GITHUB_TOKEN }} commit-message: autopep8 action fixes @@ -272,7 +268,6 @@ jobs: body: This is an auto-generated PR with fixes by autopep8. labels: autopep8, automated pr branch: ${{ steps.vars.outputs.branch-name }} - branch-suffix: none - name: Fail if autopep8 made changes if: steps.autopep8.outputs.exit-code == 2 run: exit 1 @@ -300,13 +295,13 @@ jobs: if: startsWith(github.ref, 'refs/heads/') runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 ... someOtherJob: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 ... ``` @@ -324,7 +319,7 @@ The recommended method is to use [`set-output`](https://help.github.com/en/githu echo ::set-output name=pr_body::"This PR was auto-generated on $(date +%d-%m-%Y) \ by [create-pull-request](https://github.com/peter-evans/create-pull-request)." - name: Create Pull Request - uses: peter-evans/create-pull-request@v1 + uses: peter-evans/create-pull-request@v2-beta with: token: ${{ secrets.GITHUB_TOKEN }} title: ${{ steps.vars.outputs.pr_title }} @@ -340,7 +335,7 @@ Alternatively, [`set-env`](https://help.github.com/en/github/automating-your-wor echo ::set-env name=PULL_REQUEST_BODY::"This PR was auto-generated on $(date +%d-%m-%Y) \ by [create-pull-request](https://github.com/peter-evans/create-pull-request)." - name: Create Pull Request - uses: peter-evans/create-pull-request@v1 + uses: peter-evans/create-pull-request@v2-beta with: token: ${{ secrets.GITHUB_TOKEN }} title: ${{ env.PULL_REQUEST_TITLE }} From fb48683668ea86ef31c5d4eb22c4adb18dba711c Mon Sep 17 00:00:00 2001 From: Peter Evans Date: Sun, 29 Dec 2019 16:41:59 +0900 Subject: [PATCH 16/30] Fix bug when symbolic ref fails --- dist/src/create_pull_request.py | 4 ++-- src/create_pull_request.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dist/src/create_pull_request.py b/dist/src/create_pull_request.py index e5a02e4..3578da0 100755 --- a/dist/src/create_pull_request.py +++ b/dist/src/create_pull_request.py @@ -114,8 +114,8 @@ repo = Repo(os.getcwd()) # - HEAD is a tag try: working_base = repo.git.symbolic_ref("HEAD", "--short") -except: - print(f"::debug::{working_base}") +except GitCommandError as e: + print(f"::debug::{e.stderr}") print( f"::error::The checked out ref is not a valid base for a pull request. " + "Unable to continue. Exiting." diff --git a/src/create_pull_request.py b/src/create_pull_request.py index e5a02e4..3578da0 100755 --- a/src/create_pull_request.py +++ b/src/create_pull_request.py @@ -114,8 +114,8 @@ repo = Repo(os.getcwd()) # - HEAD is a tag try: working_base = repo.git.symbolic_ref("HEAD", "--short") -except: - print(f"::debug::{working_base}") +except GitCommandError as e: + print(f"::debug::{e.stderr}") print( f"::error::The checked out ref is not a valid base for a pull request. " + "Unable to continue. Exiting." From 95615643f077d9869230c2e3716b90220490e543 Mon Sep 17 00:00:00 2001 From: Peter Evans Date: Sun, 29 Dec 2019 17:33:07 +0900 Subject: [PATCH 17/30] Add updating doc --- updating.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 updating.md diff --git a/updating.md b/updating.md new file mode 100644 index 0000000..09e8696 --- /dev/null +++ b/updating.md @@ -0,0 +1,25 @@ +# Updating from `v1` to `v2` + +## Breaking changes + +- `v2` now expects repositories to be checked out with `actions/checkout@v2` + + To use `actions/checkout@v1` the following step to checkout the branch is necessary. + ``` + - uses: actions/checkout@v1 + - name: Checkout branch + run: git checkout "${GITHUB_REF:11}" + ``` + +- The two branch naming strategies have been swapped. Fixed branch naming strategy is now the default. i.e. `branch-suffix: none` is now the default and should be removed from configuration if set. + +- `author-name`, `author-email`, `committer-name`, `committer-email` have been removed in favour of `author` and `committer`. + They can both be set in the format `Display Name ` + + If neither `author` or `committer` are set the action will default to making commits as the GitHub Actions bot user. + +## New features + +- Unpushed commits made during the workflow before the action runs will now be considered as changes to be raised in the pull request +- New commits made to the pull request base will now be taken into account when pull requests are updated +- If an updated pull request no longer differs from its base it will automatically be closed and the pull request branch deleted From b7505897473be31dfbba0c51a018d8cbd9b69095 Mon Sep 17 00:00:00 2001 From: Peter Evans Date: Mon, 30 Dec 2019 11:10:22 +0900 Subject: [PATCH 18/30] Update README --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 10d9700..ca0440b 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,17 @@ Note that in order to read the step output the action step must have an id. echo "Pull Request Number - ${{ steps.cpr.outputs.pr_number }}" ``` +### Checkout + +This action expects repositories to be checked out with `actions/checkout@v2`. + +If there is some reason you need to use `actions/checkout@v1` the following step can be added to checkout the branch. + +```yml + - uses: actions/checkout@v1 + - run: git checkout "${GITHUB_REF:11}" +``` + ### Branch naming For branch naming there are two strategies. Create a fixed-name pull request branch that will be updated with new changes until it is merged or closed, OR, always create a new unique branch each time there are changes to be committed. From 0af5090b86b0699803ddc8ecdacb521d99c40499 Mon Sep 17 00:00:00 2001 From: Peter Evans Date: Mon, 30 Dec 2019 12:11:55 +0900 Subject: [PATCH 19/30] Update README --- README.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/README.md b/README.md index ca0440b..837fcc9 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,30 @@ In most cases, where the committer and author are the same, just the committer c committer: Peter Evans ``` +### Controlling commits + +As well as relying on the action to handle uncommitted changes, you can additionally make your own commits before the action runs. + +```yml + steps: + - uses: actions/checkout@v2 + - name: Create commits + run: | + git config user.name 'Peter Evans' + git config user.email 'peter-evans@users.noreply.github.com' + date +%s > report.txt + git commit -am "Modify tracked file during workflow" + date +%s > new-report.txt + git add -A + git commit -m "Add untracked file during workflow" + - name: Uncommitted change + run: date +%s > report.txt + - name: Create Pull Request + uses: peter-evans/create-pull-request@v2-beta + with: + token: ${{ secrets.GITHUB_TOKEN }} +``` + ## Reference Example The following workflow is a reference example that sets all the main inputs. From ba798237493c6343689f1633c7a58ed86477007f Mon Sep 17 00:00:00 2001 From: Peter Evans Date: Mon, 30 Dec 2019 12:36:39 +0900 Subject: [PATCH 20/30] Update examples --- examples.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/examples.md b/examples.md index 10237af..7dc9a37 100644 --- a/examples.md +++ b/examples.md @@ -225,7 +225,9 @@ An `on: repository_dispatch` workflow can be triggered from another workflow wit ## Use case: Create a pull request to modify/fix pull requests -This is a pattern that works well for any automated code linting and fixing. A pull request can be created to fix or modify something during an `on: pull_request` workflow. The pull request containing the fix will be raised with the original pull request as the base. This can be then be merged to update the original pull request and pass any required tests. +**Note**: While the following approach does work in some cases, my strong recommendation would be to use a slash command style "ChatOps" solution for operations on pull requests. See [slash-command-dispatch](https://github.com/peter-evans/slash-command-dispatch) for such a solution. + +This is a pattern that lends itself to automated code linting and fixing. A pull request can be created to fix or modify something during an `on: pull_request` workflow. The pull request containing the fix will be raised with the original pull request as the base. This can be then be merged to update the original pull request and pass any required tests. Note that due to [limitations on forked repositories](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/authenticating-with-the-github_token#permissions-for-the-github_token) workflows for this use case do not work for pull requests raised from forks. @@ -250,6 +252,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 + with: + ref: ${{ github.head_ref }} - name: autopep8 id: autopep8 uses: peter-evans/autopep8@v1.1.0 @@ -257,7 +261,7 @@ jobs: args: --exit-code --recursive --in-place --aggressive --aggressive . - name: Set autopep8 branch name id: vars - run: echo ::set-output name=branch-name::"autopep8-patches/$GITHUB_HEAD_REF" + run: echo ::set-output name=branch-name::"autopep8-patches/${{ github.head_ref }}" - name: Create Pull Request if: steps.autopep8.outputs.exit-code == 2 uses: peter-evans/create-pull-request@v2-beta From 02f0ffd5583e5b0e915c057f693a84d477855ba8 Mon Sep 17 00:00:00 2001 From: Peter Evans Date: Tue, 31 Dec 2019 12:02:17 +0900 Subject: [PATCH 21/30] Update test fixture --- src/test_create_or_update_branch.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test_create_or_update_branch.py b/src/test_create_or_update_branch.py index 2e2209b..fb825cb 100644 --- a/src/test_create_or_update_branch.py +++ b/src/test_create_or_update_branch.py @@ -93,6 +93,7 @@ def before_after_all(): assert not repo.is_dirty(untracked_files=True) # Create a new default branch for the test run + repo.remotes.origin.fetch() repo.git.checkout("master") repo.git.checkout("HEAD", b=NOT_BASE_BRANCH) create_tracked_change() From c388aba95ab6655ad425cc113c93484cac9f2012 Mon Sep 17 00:00:00 2001 From: Peter Evans Date: Tue, 31 Dec 2019 12:33:03 +0900 Subject: [PATCH 22/30] Add env override for repo path --- src/test_create_or_update_branch.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/test_create_or_update_branch.py b/src/test_create_or_update_branch.py index fb825cb..ba12c26 100644 --- a/src/test_create_or_update_branch.py +++ b/src/test_create_or_update_branch.py @@ -8,12 +8,15 @@ import sys import time +# Set git repo +repo_path = os.getenv("COUB_REPO_PATH", os.getcwd()) +repo = Repo(repo_path) + # Set git environment author_name = "github-actions[bot]" author_email = "41898282+github-actions[bot]@users.noreply.github.com" committer_name = "GitHub" committer_email = "noreply@github.com" -repo = Repo(os.getcwd()) repo.git.update_environment( GIT_AUTHOR_NAME=author_name, GIT_AUTHOR_EMAIL=author_email, From cff82c7ec9942da37623a6cda01d5b32c29924a1 Mon Sep 17 00:00:00 2001 From: Peter Evans Date: Tue, 31 Dec 2019 12:57:14 +0900 Subject: [PATCH 23/30] Update fixture paths --- src/test_create_or_update_branch.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/test_create_or_update_branch.py b/src/test_create_or_update_branch.py index ba12c26..a0f400c 100644 --- a/src/test_create_or_update_branch.py +++ b/src/test_create_or_update_branch.py @@ -9,8 +9,8 @@ import time # Set git repo -repo_path = os.getenv("COUB_REPO_PATH", os.getcwd()) -repo = Repo(repo_path) +REPO_PATH = os.getenv("COUB_REPO_PATH", os.getcwd()) +repo = Repo(REPO_PATH) # Set git environment author_name = "github-actions[bot]" @@ -42,7 +42,7 @@ def create_tracked_change(content=None): if content is None: content = str(time.time()) # Create a tracked file change - with open(TRACKED_FILE, "w") as f: + with open(os.path.join(REPO_PATH, TRACKED_FILE), "w") as f: f.write(content) return content @@ -51,20 +51,20 @@ def create_untracked_change(content=None): if content is None: content = str(time.time()) # Create an untracked file change - with open(UNTRACKED_FILE, "w") as f: + with open(os.path.join(REPO_PATH, UNTRACKED_FILE), "w") as f: f.write(content) return content def get_tracked_content(): # Read the content of the tracked file - with open(TRACKED_FILE, "r") as f: + with open(os.path.join(REPO_PATH, TRACKED_FILE), "r") as f: return f.read() def get_untracked_content(): # Read the content of the untracked file - with open(UNTRACKED_FILE, "r") as f: + with open(os.path.join(REPO_PATH, UNTRACKED_FILE), "r") as f: return f.read() From e8e53ed4316ea3b0c8b6ec160d0a5ed2114d7f89 Mon Sep 17 00:00:00 2001 From: Peter Evans Date: Tue, 31 Dec 2019 13:40:46 +0900 Subject: [PATCH 24/30] Update fixture paths --- dist/src/test_create_or_update_branch.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/dist/src/test_create_or_update_branch.py b/dist/src/test_create_or_update_branch.py index 2e2209b..a0f400c 100644 --- a/dist/src/test_create_or_update_branch.py +++ b/dist/src/test_create_or_update_branch.py @@ -8,12 +8,15 @@ import sys import time +# Set git repo +REPO_PATH = os.getenv("COUB_REPO_PATH", os.getcwd()) +repo = Repo(REPO_PATH) + # Set git environment author_name = "github-actions[bot]" author_email = "41898282+github-actions[bot]@users.noreply.github.com" committer_name = "GitHub" committer_email = "noreply@github.com" -repo = Repo(os.getcwd()) repo.git.update_environment( GIT_AUTHOR_NAME=author_name, GIT_AUTHOR_EMAIL=author_email, @@ -39,7 +42,7 @@ def create_tracked_change(content=None): if content is None: content = str(time.time()) # Create a tracked file change - with open(TRACKED_FILE, "w") as f: + with open(os.path.join(REPO_PATH, TRACKED_FILE), "w") as f: f.write(content) return content @@ -48,20 +51,20 @@ def create_untracked_change(content=None): if content is None: content = str(time.time()) # Create an untracked file change - with open(UNTRACKED_FILE, "w") as f: + with open(os.path.join(REPO_PATH, UNTRACKED_FILE), "w") as f: f.write(content) return content def get_tracked_content(): # Read the content of the tracked file - with open(TRACKED_FILE, "r") as f: + with open(os.path.join(REPO_PATH, TRACKED_FILE), "r") as f: return f.read() def get_untracked_content(): # Read the content of the untracked file - with open(UNTRACKED_FILE, "r") as f: + with open(os.path.join(REPO_PATH, UNTRACKED_FILE), "r") as f: return f.read() @@ -93,6 +96,7 @@ def before_after_all(): assert not repo.is_dirty(untracked_files=True) # Create a new default branch for the test run + repo.remotes.origin.fetch() repo.git.checkout("master") repo.git.checkout("HEAD", b=NOT_BASE_BRANCH) create_tracked_change() From 11c388252c5289330e142dfa92eb12da56df4273 Mon Sep 17 00:00:00 2001 From: Peter Evans Date: Fri, 3 Jan 2020 09:33:16 +0900 Subject: [PATCH 25/30] Add concepts and guidelines doc --- concepts-guidelines.md | 107 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 concepts-guidelines.md diff --git a/concepts-guidelines.md b/concepts-guidelines.md new file mode 100644 index 0000000..6abaf4e --- /dev/null +++ b/concepts-guidelines.md @@ -0,0 +1,107 @@ +# Concepts and guidelines + +This document covers terminology, how the action works, and general usage guidelines. + +- [Terminology](#terminology) +- [Events and checkout](#events-and-checkout) +- [How the action works](#how-the-action-works) +- [Guidelines](#guidelines) + - [Providing a consistent base](#providing-a-consistent-base) + - [Pull request events](#pull-request-events) + - [Restrictions on forked repositories](#restrictions-on-forked-repositories) + +## Terminology + +[Pull requests](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests#about-pull-requests) are proposed changes to a repository branch that can be reviewed by a repository's collaborators before being accepted or rejected. + +A pull request references two branches: + +- The `base` of a pull request is the branch you intend to change once the proposed changes are merged. +- The `branch` of a pull request represents what you intend the `base` to look like when merged. It is the `base` branch *plus* changes that have been made to it. + +## Events and checkout + +For each [event type](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/events-that-trigger-workflows) there is a default `GITHUB_SHA` that will be checked out by the GitHub Actions [checkout](https://github.com/actions/checkout) action. + +The majority of events will default to checking out the "last commit on default branch," which in most cases will be the latest commit on `master`. + +The default can be overridden by specifying a `ref` on checkout. + +```yml + - uses: actions/checkout@v2 + with: + ref: master +``` + +## How the action works + +By default, the action expects to be executed on the pull request `base`—the branch you intend to modify with the proposed changes. + +Workflow steps: + +1. Checkout the `base` branch +2. Make changes +3. Execute `create-pull-request` action + +The following git diagram shows how the action creates and updates a pull request branch. + +WIP + +## Guidelines + +### Providing a consistent base + +For the action to work correctly it should be executed in a workflow that checks out a *consistent base* branch. This will be the base of the pull request unless overridden with the `base` input. + +This means your workflow should be consistently checking out the branch that you intend to modify once the PR is merged. + +In the following example, the [`push`](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/events-that-trigger-workflows#push-event-push) and [`create`](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/events-that-trigger-workflows#create-event-create) events both trigger the same workflow. This will cause the checkout action to checkout commits from inconsistent branches. Do *not* do this. It will cause multiple pull requests to be created for each additional `base` the action is executed against. + +```yml +on: + push: + create: +jobs: + example: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 +``` + +Although rare, there may be use cases where it makes sense to execute the workflow on a branch that is not the base of the pull request. In these cases, the base branch can be specified with the `base` action input. The action will attempt to rebase changes made during the workflow on to the actual base. + +### Pull request events + +Workflows triggered by `pull_request` events will by default check out a [merge commit](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/events-that-trigger-workflows#pull-request-event-pull_request). To prevent the merge commit being included in created pull requests it is necessary to checkout the `head_ref`. + +```yml + - uses: actions/checkout@v2 + with: + ref: ${{ github.head_ref }} +``` + +### Restrictions on forked repositories + +GitHub Actions have imposed restrictions on events triggered by a forked repository. For example, the `pull_request` event triggered by a fork opening a pull request in the upstream repository. + +- Events from forks cannot access secrets, except for for the default `GITHUB_TOKEN`. + > With the exception of GITHUB_TOKEN, secrets are not passed to the runner when a workflow is triggered from a forked repository. + + [GitHub Actions: Using encrypted secrets in a workflow](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets#using-encrypted-secrets-in-a-workflow) + +- The `GITHUB_TOKEN` has read-only access when an event is triggered by a forked repository. + + [GitHub Actions: Permissions for the GITHUB_TOKEN](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/authenticating-with-the-github_token#permissions-for-the-github_token) + +These restrictions mean that during a `pull_request` event triggered by a forked repository the action will be unable to commit changes to a branch. + +A job condition can be added to prevent workflows from executing when triggered by a repository fork. + +```yml +on: pull_request +jobs: + example: + runs-on: ubuntu-latest + # Check if the event is not triggered by a fork + if: github.event.pull_request.head.repo.full_name == github.repository +``` From c8fed30c659bb9141dc6621998566c5adb177d5f Mon Sep 17 00:00:00 2001 From: Peter Evans Date: Fri, 3 Jan 2020 09:51:43 +0900 Subject: [PATCH 26/30] Move docs --- README.md | 10 +++++++--- concepts-guidelines.md => docs/concepts-guidelines.md | 0 examples.md => docs/examples.md | 0 updating.md => docs/updating.md | 6 +++--- 4 files changed, 10 insertions(+), 6 deletions(-) rename concepts-guidelines.md => docs/concepts-guidelines.md (100%) rename examples.md => docs/examples.md (100%) rename updating.md => docs/updating.md (87%) diff --git a/README.md b/README.md index 837fcc9..190a30e 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,13 @@ Create Pull Request action will: 2. Commit all changes to a new branch, or update an existing pull request branch. 3. Create a pull request to merge the new branch into the base—the branch checked out in the workflow. -## Usage +## Documentation -See [examples](examples.md) for detailed use cases. +- [Concepts and guidelines](docs/concepts-guidelines.md) +- [Examples](docs/examples.md) +- [Updating from v1](docs/updating.md) + +## Usage ```yml - name: Create Pull Request @@ -146,7 +150,7 @@ As well as relying on the action to handle uncommitted changes, you can addition The following workflow is a reference example that sets all the main inputs. -See [examples](examples.md) for more realistic use cases. +See [examples](docs/examples.md) for more realistic use cases. ```yml name: Create Pull Request diff --git a/concepts-guidelines.md b/docs/concepts-guidelines.md similarity index 100% rename from concepts-guidelines.md rename to docs/concepts-guidelines.md diff --git a/examples.md b/docs/examples.md similarity index 100% rename from examples.md rename to docs/examples.md diff --git a/updating.md b/docs/updating.md similarity index 87% rename from updating.md rename to docs/updating.md index 09e8696..6ae1ec2 100644 --- a/updating.md +++ b/docs/updating.md @@ -20,6 +20,6 @@ ## New features -- Unpushed commits made during the workflow before the action runs will now be considered as changes to be raised in the pull request -- New commits made to the pull request base will now be taken into account when pull requests are updated -- If an updated pull request no longer differs from its base it will automatically be closed and the pull request branch deleted +- Unpushed commits made during the workflow before the action runs will now be considered as changes to be raised in the pull request. See [Controlling commits](https://github.com/peter-evans/create-pull-request/tree/v2-beta#controlling-commits) for details. +- New commits made to the pull request base will now be taken into account when pull requests are updated. +- If an updated pull request no longer differs from its base it will automatically be closed and the pull request branch deleted. From 8e07307f04aaaf01cda1f6ff68b2b615da929206 Mon Sep 17 00:00:00 2001 From: Peter Evans Date: Fri, 3 Jan 2020 10:05:05 +0900 Subject: [PATCH 27/30] Remove multi workflow --- .../workflows/create-pull-request-multi.yml | 45 ------------------- 1 file changed, 45 deletions(-) delete mode 100644 .github/workflows/create-pull-request-multi.yml diff --git a/.github/workflows/create-pull-request-multi.yml b/.github/workflows/create-pull-request-multi.yml deleted file mode 100644 index 89b2120..0000000 --- a/.github/workflows/create-pull-request-multi.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: Create Pull Request All Platforms -on: - repository_dispatch: - types: [create-pull-request-multi] -jobs: - createPullRequest: - name: CPR on ${{ matrix.platform }} - strategy: - matrix: - platform: [ubuntu-latest, macos-latest, windows-latest] - runs-on: ${{ matrix.platform }} - steps: - - uses: actions/checkout@v2 - - name: Create report file - if: matrix.platform == 'ubuntu-latest' || matrix.platform == 'macos-latest' - run: date +%s > report.txt - - name: Create report file (windows) - if: matrix.platform == 'windows-latest' - run: echo %DATE% %TIME% > report.txt - - name: Create Pull Request - id: cpr - uses: ./ - with: - token: ${{ secrets.GITHUB_TOKEN }} - commit-message: Add report file - committer: Peter Evans - title: '[Example] Add report file' - body: | - New report - - Contains *today's* date - - Auto-generated by [create-pull-request][1] - - [1]: https://github.com/peter-evans/create-pull-request - labels: report, automated pr - assignees: peter-evans - reviewers: peter-evans - milestone: 1 - project: Example Project - project-column: To do - branch: example-patches - branch-suffix: random - - name: Check outputs - run: | - echo "Pull Request Number - ${{ env.PULL_REQUEST_NUMBER }}" - echo "Pull Request Number - ${{ steps.cpr.outputs.pr_number }}" From 430f28afbfc19e8d5be6c4b1fd44796dd112f4e9 Mon Sep 17 00:00:00 2001 From: Peter Evans Date: Fri, 3 Jan 2020 11:53:45 +0900 Subject: [PATCH 28/30] Add gitgraph to docs --- assets/cpr-gitgraph.htm | 68 ++++++++++++++++++++++++++++++++++++ assets/cpr-gitgraph.png | Bin 0 -> 211796 bytes docs/concepts-guidelines.md | 2 +- 3 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 assets/cpr-gitgraph.htm create mode 100644 assets/cpr-gitgraph.png diff --git a/assets/cpr-gitgraph.htm b/assets/cpr-gitgraph.htm new file mode 100644 index 0000000..39e53ba --- /dev/null +++ b/assets/cpr-gitgraph.htm @@ -0,0 +1,68 @@ + + + + + + create-pull-request GitHub action + + + + +
+ + + + + + + \ No newline at end of file diff --git a/assets/cpr-gitgraph.png b/assets/cpr-gitgraph.png new file mode 100644 index 0000000000000000000000000000000000000000..aa0000359679a211c85ae9eb2bb03006485b6be7 GIT binary patch literal 211796 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 JH)6=rY(8z=4ZX1rf8fzZIEyiR*2&c=)L1B? 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 Date: Fri, 3 Jan 2020 12:32:22 +0900 Subject: [PATCH 29/30] Update npm packages --- dist/index.js | 1898 ++++++++++++++++++++++++++++++++++----------- package-lock.json | 23 +- 2 files changed, 1474 insertions(+), 447 deletions(-) diff --git a/dist/index.js b/dist/index.js index 2c373c8..1890c82 100644 --- a/dist/index.js +++ b/dist/index.js @@ -358,6 +358,9 @@ Object.defineProperty(exports, "__esModule", { value: true }); const os = __webpack_require__(87); const events = __webpack_require__(614); const child = __webpack_require__(129); +const path = __webpack_require__(622); +const io = __webpack_require__(1); +const ioUtil = __webpack_require__(672); /* eslint-disable @typescript-eslint/unbound-method */ const IS_WINDOWS = process.platform === 'win32'; /* @@ -703,6 +706,16 @@ class ToolRunner extends events.EventEmitter { */ exec() { return __awaiter(this, void 0, void 0, function* () { + // root the tool path if it is unrooted and contains relative pathing + if (!ioUtil.isRooted(this.toolPath) && + (this.toolPath.includes('/') || + (IS_WINDOWS && this.toolPath.includes('\\')))) { + // prefer options.cwd if it is specified, however options.cwd may also need to be rooted + this.toolPath = path.resolve(process.cwd(), this.options.cwd || process.cwd(), this.toolPath); + } + // if the tool is only a file name, then resolve it from the PATH + // otherwise verify it exists (add extension on Windows if necessary) + this.toolPath = yield io.which(this.toolPath, true); return new Promise((resolve, reject) => { this._debug(`exec tool: ${this.toolPath}`); this._debug('arguments:'); @@ -919,6 +932,40 @@ class ExecState extends events.EventEmitter { } //# sourceMappingURL=toolrunner.js.map +/***/ }), + +/***/ 13: +/***/ (function(module, __unusedexports, __webpack_require__) { + +"use strict"; + + +var replace = String.prototype.replace; +var percentTwenties = /%20/g; + +var util = __webpack_require__(581); + +var Format = { + RFC1738: 'RFC1738', + RFC3986: 'RFC3986' +}; + +module.exports = util.assign( + { + 'default': Format.RFC3986, + formatters: { + RFC1738: function (value) { + return replace.call(value, percentTwenties, '+'); + }, + RFC3986: function (value) { + return String(value); + } + } + }, + Format +); + + /***/ }), /***/ 16: @@ -2961,6 +3008,25 @@ function coerce (version, options) { module.exports = require("assert"); +/***/ }), + +/***/ 386: +/***/ (function(module, __unusedexports, __webpack_require__) { + +"use strict"; + + +var stringify = __webpack_require__(897); +var parse = __webpack_require__(755); +var formats = __webpack_require__(13); + +module.exports = { + formats: formats, + parse: parse, + stringify: stringify +}; + + /***/ }), /***/ 413: @@ -3696,6 +3762,250 @@ function _evaluateVersions(versions, versionSpec) { } //# sourceMappingURL=tool-cache.js.map +/***/ }), + +/***/ 581: +/***/ (function(module) { + +"use strict"; + + +var has = Object.prototype.hasOwnProperty; +var isArray = Array.isArray; + +var hexTable = (function () { + var array = []; + for (var i = 0; i < 256; ++i) { + array.push('%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase()); + } + + return array; +}()); + +var compactQueue = function compactQueue(queue) { + while (queue.length > 1) { + var item = queue.pop(); + var obj = item.obj[item.prop]; + + if (isArray(obj)) { + var compacted = []; + + for (var j = 0; j < obj.length; ++j) { + if (typeof obj[j] !== 'undefined') { + compacted.push(obj[j]); + } + } + + item.obj[item.prop] = compacted; + } + } +}; + +var arrayToObject = function arrayToObject(source, options) { + var obj = options && options.plainObjects ? Object.create(null) : {}; + for (var i = 0; i < source.length; ++i) { + if (typeof source[i] !== 'undefined') { + obj[i] = source[i]; + } + } + + return obj; +}; + +var merge = function merge(target, source, options) { + /* eslint no-param-reassign: 0 */ + if (!source) { + return target; + } + + if (typeof source !== 'object') { + if (isArray(target)) { + target.push(source); + } else if (target && typeof target === 'object') { + if ((options && (options.plainObjects || options.allowPrototypes)) || !has.call(Object.prototype, source)) { + target[source] = true; + } + } else { + return [target, source]; + } + + return target; + } + + if (!target || typeof target !== 'object') { + return [target].concat(source); + } + + var mergeTarget = target; + if (isArray(target) && !isArray(source)) { + mergeTarget = arrayToObject(target, options); + } + + if (isArray(target) && isArray(source)) { + source.forEach(function (item, i) { + if (has.call(target, i)) { + var targetItem = target[i]; + if (targetItem && typeof targetItem === 'object' && item && typeof item === 'object') { + target[i] = merge(targetItem, item, options); + } else { + target.push(item); + } + } else { + target[i] = item; + } + }); + return target; + } + + return Object.keys(source).reduce(function (acc, key) { + var value = source[key]; + + if (has.call(acc, key)) { + acc[key] = merge(acc[key], value, options); + } else { + acc[key] = value; + } + return acc; + }, mergeTarget); +}; + +var assign = function assignSingleSource(target, source) { + return Object.keys(source).reduce(function (acc, key) { + acc[key] = source[key]; + return acc; + }, target); +}; + +var decode = function (str, decoder, charset) { + var strWithoutPlus = str.replace(/\+/g, ' '); + if (charset === 'iso-8859-1') { + // unescape never throws, no try...catch needed: + return strWithoutPlus.replace(/%[0-9a-f]{2}/gi, unescape); + } + // utf-8 + try { + return decodeURIComponent(strWithoutPlus); + } catch (e) { + return strWithoutPlus; + } +}; + +var encode = function encode(str, defaultEncoder, charset) { + // This code was originally written by Brian White (mscdex) for the io.js core querystring library. + // It has been adapted here for stricter adherence to RFC 3986 + if (str.length === 0) { + return str; + } + + var string = str; + if (typeof str === 'symbol') { + string = Symbol.prototype.toString.call(str); + } else if (typeof str !== 'string') { + string = String(str); + } + + if (charset === 'iso-8859-1') { + return escape(string).replace(/%u[0-9a-f]{4}/gi, function ($0) { + return '%26%23' + parseInt($0.slice(2), 16) + '%3B'; + }); + } + + var out = ''; + for (var i = 0; i < string.length; ++i) { + var c = string.charCodeAt(i); + + if ( + c === 0x2D // - + || c === 0x2E // . + || c === 0x5F // _ + || c === 0x7E // ~ + || (c >= 0x30 && c <= 0x39) // 0-9 + || (c >= 0x41 && c <= 0x5A) // a-z + || (c >= 0x61 && c <= 0x7A) // A-Z + ) { + out += string.charAt(i); + continue; + } + + if (c < 0x80) { + out = out + hexTable[c]; + continue; + } + + if (c < 0x800) { + out = out + (hexTable[0xC0 | (c >> 6)] + hexTable[0x80 | (c & 0x3F)]); + continue; + } + + if (c < 0xD800 || c >= 0xE000) { + out = out + (hexTable[0xE0 | (c >> 12)] + hexTable[0x80 | ((c >> 6) & 0x3F)] + hexTable[0x80 | (c & 0x3F)]); + continue; + } + + i += 1; + c = 0x10000 + (((c & 0x3FF) << 10) | (string.charCodeAt(i) & 0x3FF)); + out += hexTable[0xF0 | (c >> 18)] + + hexTable[0x80 | ((c >> 12) & 0x3F)] + + hexTable[0x80 | ((c >> 6) & 0x3F)] + + hexTable[0x80 | (c & 0x3F)]; + } + + return out; +}; + +var compact = function compact(value) { + var queue = [{ obj: { o: value }, prop: 'o' }]; + var refs = []; + + for (var i = 0; i < queue.length; ++i) { + var item = queue[i]; + var obj = item.obj[item.prop]; + + var keys = Object.keys(obj); + for (var j = 0; j < keys.length; ++j) { + var key = keys[j]; + var val = obj[key]; + if (typeof val === 'object' && val !== null && refs.indexOf(val) === -1) { + queue.push({ obj: obj, prop: key }); + refs.push(val); + } + } + } + + compactQueue(queue); + + return value; +}; + +var isRegExp = function isRegExp(obj) { + return Object.prototype.toString.call(obj) === '[object RegExp]'; +}; + +var isBuffer = function isBuffer(obj) { + if (!obj || typeof obj !== 'object') { + return false; + } + + return !!(obj.constructor && obj.constructor.isBuffer && obj.constructor.isBuffer(obj)); +}; + +var combine = function combine(a, b) { + return [].concat(a, b); +}; + +module.exports = { + arrayToObject: arrayToObject, + assign: assign, + combine: combine, + compact: compact, + decode: decode, + encode: encode, + isBuffer: isBuffer, + isRegExp: isRegExp, + merge: merge +}; + + /***/ }), /***/ 605: @@ -3964,6 +4274,132 @@ function bytesToUuid(buf, offset) { module.exports = bytesToUuid; +/***/ }), + +/***/ 729: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +"use strict"; + +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const qs = __webpack_require__(386); +const url = __webpack_require__(835); +const path = __webpack_require__(622); +const zlib = __webpack_require__(761); +/** + * creates an url from a request url and optional base url (http://server:8080) + * @param {string} resource - a fully qualified url or relative path + * @param {string} baseUrl - an optional baseUrl (http://server:8080) + * @param {IRequestOptions} options - an optional options object, could include QueryParameters e.g. + * @return {string} - resultant url + */ +function getUrl(resource, baseUrl, queryParams) { + const pathApi = path.posix || path; + let requestUrl = ''; + if (!baseUrl) { + requestUrl = resource; + } + else if (!resource) { + requestUrl = baseUrl; + } + else { + const base = url.parse(baseUrl); + const resultantUrl = url.parse(resource); + // resource (specific per request) elements take priority + resultantUrl.protocol = resultantUrl.protocol || base.protocol; + resultantUrl.auth = resultantUrl.auth || base.auth; + resultantUrl.host = resultantUrl.host || base.host; + resultantUrl.pathname = pathApi.resolve(base.pathname, resultantUrl.pathname); + if (!resultantUrl.pathname.endsWith('/') && resource.endsWith('/')) { + resultantUrl.pathname += '/'; + } + requestUrl = url.format(resultantUrl); + } + return queryParams ? + getUrlWithParsedQueryParams(requestUrl, queryParams) : + requestUrl; +} +exports.getUrl = getUrl; +/** + * + * @param {string} requestUrl + * @param {IRequestQueryParams} queryParams + * @return {string} - Request's URL with Query Parameters appended/parsed. + */ +function getUrlWithParsedQueryParams(requestUrl, queryParams) { + const url = requestUrl.replace(/\?$/g, ''); // Clean any extra end-of-string "?" character + const parsedQueryParams = qs.stringify(queryParams.params, buildParamsStringifyOptions(queryParams)); + return `${url}${parsedQueryParams}`; +} +/** + * Build options for QueryParams Stringifying. + * + * @param {IRequestQueryParams} queryParams + * @return {object} + */ +function buildParamsStringifyOptions(queryParams) { + let options = { + addQueryPrefix: true, + delimiter: (queryParams.options || {}).separator || '&', + allowDots: (queryParams.options || {}).shouldAllowDots || false, + arrayFormat: (queryParams.options || {}).arrayFormat || 'repeat', + encodeValuesOnly: (queryParams.options || {}).shouldOnlyEncodeValues || true + }; + return options; +} +/** + * Decompress/Decode gzip encoded JSON + * Using Node.js built-in zlib module + * + * @param {Buffer} buffer + * @param {string} charset? - optional; defaults to 'utf-8' + * @return {Promise} + */ +function decompressGzippedContent(buffer, charset) { + return __awaiter(this, void 0, void 0, function* () { + return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { + zlib.gunzip(buffer, function (error, buffer) { + if (error) { + reject(error); + } + resolve(buffer.toString(charset || 'utf-8')); + }); + })); + }); +} +exports.decompressGzippedContent = decompressGzippedContent; +/** + * Obtain Response's Content Charset. + * Through inspecting `content-type` response header. + * It Returns 'utf-8' if NO charset specified/matched. + * + * @param {IHttpClientResponse} response + * @return {string} - Content Encoding Charset; Default=utf-8 + */ +function obtainContentCharset(response) { + // Find the charset, if specified. + // Search for the `charset=CHARSET` string, not including `;,\r\n` + // Example: content-type: 'application/json;charset=utf-8' + // |__ matches would be ['charset=utf-8', 'utf-8', index: 18, input: 'application/json; charset=utf-8'] + // |_____ matches[1] would have the charset :tada: , in our example it's utf-8 + // However, if the matches Array was empty or no charset found, 'utf-8' would be returned by default. + const contentType = response.message.headers['content-type'] || ''; + const matches = contentType.match(/charset=([^;,\r\n]+)/i); + return (matches && matches[1]) ? matches[1] : 'utf-8'; +} +exports.obtainContentCharset = obtainContentCharset; + + /***/ }), /***/ 747: @@ -3973,6 +4409,269 @@ module.exports = require("fs"); /***/ }), +/***/ 755: +/***/ (function(module, __unusedexports, __webpack_require__) { + +"use strict"; + + +var utils = __webpack_require__(581); + +var has = Object.prototype.hasOwnProperty; +var isArray = Array.isArray; + +var defaults = { + allowDots: false, + allowPrototypes: false, + arrayLimit: 20, + charset: 'utf-8', + charsetSentinel: false, + comma: false, + decoder: utils.decode, + delimiter: '&', + depth: 5, + ignoreQueryPrefix: false, + interpretNumericEntities: false, + parameterLimit: 1000, + parseArrays: true, + plainObjects: false, + strictNullHandling: false +}; + +var interpretNumericEntities = function (str) { + return str.replace(/&#(\d+);/g, function ($0, numberStr) { + return String.fromCharCode(parseInt(numberStr, 10)); + }); +}; + +// This is what browsers will submit when the ✓ character occurs in an +// application/x-www-form-urlencoded body and the encoding of the page containing +// the form is iso-8859-1, or when the submitted form has an accept-charset +// attribute of iso-8859-1. Presumably also with other charsets that do not contain +// the ✓ character, such as us-ascii. +var isoSentinel = 'utf8=%26%2310003%3B'; // encodeURIComponent('✓') + +// These are the percent-encoded utf-8 octets representing a checkmark, indicating that the request actually is utf-8 encoded. +var charsetSentinel = 'utf8=%E2%9C%93'; // encodeURIComponent('✓') + +var parseValues = function parseQueryStringValues(str, options) { + var obj = {}; + var cleanStr = options.ignoreQueryPrefix ? str.replace(/^\?/, '') : str; + var limit = options.parameterLimit === Infinity ? undefined : options.parameterLimit; + var parts = cleanStr.split(options.delimiter, limit); + var skipIndex = -1; // Keep track of where the utf8 sentinel was found + var i; + + var charset = options.charset; + if (options.charsetSentinel) { + for (i = 0; i < parts.length; ++i) { + if (parts[i].indexOf('utf8=') === 0) { + if (parts[i] === charsetSentinel) { + charset = 'utf-8'; + } else if (parts[i] === isoSentinel) { + charset = 'iso-8859-1'; + } + skipIndex = i; + i = parts.length; // The eslint settings do not allow break; + } + } + } + + for (i = 0; i < parts.length; ++i) { + if (i === skipIndex) { + continue; + } + var part = parts[i]; + + var bracketEqualsPos = part.indexOf(']='); + var pos = bracketEqualsPos === -1 ? part.indexOf('=') : bracketEqualsPos + 1; + + var key, val; + if (pos === -1) { + key = options.decoder(part, defaults.decoder, charset, 'key'); + val = options.strictNullHandling ? null : ''; + } else { + key = options.decoder(part.slice(0, pos), defaults.decoder, charset, 'key'); + val = options.decoder(part.slice(pos + 1), defaults.decoder, charset, 'value'); + } + + if (val && options.interpretNumericEntities && charset === 'iso-8859-1') { + val = interpretNumericEntities(val); + } + + if (val && typeof val === 'string' && options.comma && val.indexOf(',') > -1) { + val = val.split(','); + } + + if (part.indexOf('[]=') > -1) { + val = isArray(val) ? [val] : val; + } + + if (has.call(obj, key)) { + obj[key] = utils.combine(obj[key], val); + } else { + obj[key] = val; + } + } + + return obj; +}; + +var parseObject = function (chain, val, options) { + var leaf = val; + + for (var i = chain.length - 1; i >= 0; --i) { + var obj; + var root = chain[i]; + + if (root === '[]' && options.parseArrays) { + obj = [].concat(leaf); + } else { + obj = options.plainObjects ? Object.create(null) : {}; + var cleanRoot = root.charAt(0) === '[' && root.charAt(root.length - 1) === ']' ? root.slice(1, -1) : root; + var index = parseInt(cleanRoot, 10); + if (!options.parseArrays && cleanRoot === '') { + obj = { 0: leaf }; + } else if ( + !isNaN(index) + && root !== cleanRoot + && String(index) === cleanRoot + && index >= 0 + && (options.parseArrays && index <= options.arrayLimit) + ) { + obj = []; + obj[index] = leaf; + } else { + obj[cleanRoot] = leaf; + } + } + + leaf = obj; + } + + return leaf; +}; + +var parseKeys = function parseQueryStringKeys(givenKey, val, options) { + if (!givenKey) { + return; + } + + // Transform dot notation to bracket notation + var key = options.allowDots ? givenKey.replace(/\.([^.[]+)/g, '[$1]') : givenKey; + + // The regex chunks + + var brackets = /(\[[^[\]]*])/; + var child = /(\[[^[\]]*])/g; + + // Get the parent + + var segment = options.depth > 0 && brackets.exec(key); + var parent = segment ? key.slice(0, segment.index) : key; + + // Stash the parent if it exists + + var keys = []; + if (parent) { + // If we aren't using plain objects, optionally prefix keys that would overwrite object prototype properties + if (!options.plainObjects && has.call(Object.prototype, parent)) { + if (!options.allowPrototypes) { + return; + } + } + + keys.push(parent); + } + + // Loop through children appending to the array until we hit depth + + var i = 0; + while (options.depth > 0 && (segment = child.exec(key)) !== null && i < options.depth) { + i += 1; + if (!options.plainObjects && has.call(Object.prototype, segment[1].slice(1, -1))) { + if (!options.allowPrototypes) { + return; + } + } + keys.push(segment[1]); + } + + // If there's a remainder, just add whatever is left + + if (segment) { + keys.push('[' + key.slice(segment.index) + ']'); + } + + return parseObject(keys, val, options); +}; + +var normalizeParseOptions = function normalizeParseOptions(opts) { + if (!opts) { + return defaults; + } + + if (opts.decoder !== null && opts.decoder !== undefined && typeof opts.decoder !== 'function') { + throw new TypeError('Decoder has to be a function.'); + } + + if (typeof opts.charset !== 'undefined' && opts.charset !== 'utf-8' && opts.charset !== 'iso-8859-1') { + throw new Error('The charset option must be either utf-8, iso-8859-1, or undefined'); + } + var charset = typeof opts.charset === 'undefined' ? defaults.charset : opts.charset; + + return { + allowDots: typeof opts.allowDots === 'undefined' ? defaults.allowDots : !!opts.allowDots, + allowPrototypes: typeof opts.allowPrototypes === 'boolean' ? opts.allowPrototypes : defaults.allowPrototypes, + arrayLimit: typeof opts.arrayLimit === 'number' ? opts.arrayLimit : defaults.arrayLimit, + charset: charset, + charsetSentinel: typeof opts.charsetSentinel === 'boolean' ? opts.charsetSentinel : defaults.charsetSentinel, + comma: typeof opts.comma === 'boolean' ? opts.comma : defaults.comma, + decoder: typeof opts.decoder === 'function' ? opts.decoder : defaults.decoder, + delimiter: typeof opts.delimiter === 'string' || utils.isRegExp(opts.delimiter) ? opts.delimiter : defaults.delimiter, + // eslint-disable-next-line no-implicit-coercion, no-extra-parens + depth: (typeof opts.depth === 'number' || opts.depth === false) ? +opts.depth : defaults.depth, + ignoreQueryPrefix: opts.ignoreQueryPrefix === true, + interpretNumericEntities: typeof opts.interpretNumericEntities === 'boolean' ? opts.interpretNumericEntities : defaults.interpretNumericEntities, + parameterLimit: typeof opts.parameterLimit === 'number' ? opts.parameterLimit : defaults.parameterLimit, + parseArrays: opts.parseArrays !== false, + plainObjects: typeof opts.plainObjects === 'boolean' ? opts.plainObjects : defaults.plainObjects, + strictNullHandling: typeof opts.strictNullHandling === 'boolean' ? opts.strictNullHandling : defaults.strictNullHandling + }; +}; + +module.exports = function (str, opts) { + var options = normalizeParseOptions(opts); + + if (str === '' || str === null || typeof str === 'undefined') { + return options.plainObjects ? Object.create(null) : {}; + } + + var tempObj = typeof str === 'string' ? parseValues(str, options) : str; + var obj = options.plainObjects ? Object.create(null) : {}; + + // Iterate over the keys and setup the new object + + var keys = Object.keys(tempObj); + for (var i = 0; i < keys.length; ++i) { + var key = keys[i]; + var newObj = parseKeys(key, tempObj[key], options); + obj = utils.merge(obj, newObj, options); + } + + return utils.compact(obj); +}; + + +/***/ }), + +/***/ 761: +/***/ (function(module) { + +module.exports = require("zlib"); + +/***/ }), + /***/ 826: /***/ (function(module, __unusedexports, __webpack_require__) { @@ -4020,461 +4719,780 @@ module.exports = require("url"); /***/ (function(__unusedmodule, exports, __webpack_require__) { "use strict"; + +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const url = __webpack_require__(835); +const http = __webpack_require__(605); +const https = __webpack_require__(211); +const util = __webpack_require__(729); +let fs; +let tunnel; +var HttpCodes; +(function (HttpCodes) { + HttpCodes[HttpCodes["OK"] = 200] = "OK"; + HttpCodes[HttpCodes["MultipleChoices"] = 300] = "MultipleChoices"; + HttpCodes[HttpCodes["MovedPermanently"] = 301] = "MovedPermanently"; + HttpCodes[HttpCodes["ResourceMoved"] = 302] = "ResourceMoved"; + HttpCodes[HttpCodes["SeeOther"] = 303] = "SeeOther"; + HttpCodes[HttpCodes["NotModified"] = 304] = "NotModified"; + HttpCodes[HttpCodes["UseProxy"] = 305] = "UseProxy"; + HttpCodes[HttpCodes["SwitchProxy"] = 306] = "SwitchProxy"; + HttpCodes[HttpCodes["TemporaryRedirect"] = 307] = "TemporaryRedirect"; + HttpCodes[HttpCodes["PermanentRedirect"] = 308] = "PermanentRedirect"; + HttpCodes[HttpCodes["BadRequest"] = 400] = "BadRequest"; + HttpCodes[HttpCodes["Unauthorized"] = 401] = "Unauthorized"; + HttpCodes[HttpCodes["PaymentRequired"] = 402] = "PaymentRequired"; + HttpCodes[HttpCodes["Forbidden"] = 403] = "Forbidden"; + HttpCodes[HttpCodes["NotFound"] = 404] = "NotFound"; + HttpCodes[HttpCodes["MethodNotAllowed"] = 405] = "MethodNotAllowed"; + HttpCodes[HttpCodes["NotAcceptable"] = 406] = "NotAcceptable"; + HttpCodes[HttpCodes["ProxyAuthenticationRequired"] = 407] = "ProxyAuthenticationRequired"; + HttpCodes[HttpCodes["RequestTimeout"] = 408] = "RequestTimeout"; + HttpCodes[HttpCodes["Conflict"] = 409] = "Conflict"; + HttpCodes[HttpCodes["Gone"] = 410] = "Gone"; + HttpCodes[HttpCodes["TooManyRequests"] = 429] = "TooManyRequests"; + HttpCodes[HttpCodes["InternalServerError"] = 500] = "InternalServerError"; + HttpCodes[HttpCodes["NotImplemented"] = 501] = "NotImplemented"; + HttpCodes[HttpCodes["BadGateway"] = 502] = "BadGateway"; + HttpCodes[HttpCodes["ServiceUnavailable"] = 503] = "ServiceUnavailable"; + HttpCodes[HttpCodes["GatewayTimeout"] = 504] = "GatewayTimeout"; +})(HttpCodes = exports.HttpCodes || (exports.HttpCodes = {})); +const HttpRedirectCodes = [HttpCodes.MovedPermanently, HttpCodes.ResourceMoved, HttpCodes.SeeOther, HttpCodes.TemporaryRedirect, HttpCodes.PermanentRedirect]; +const HttpResponseRetryCodes = [HttpCodes.BadGateway, HttpCodes.ServiceUnavailable, HttpCodes.GatewayTimeout]; +const RetryableHttpVerbs = ['OPTIONS', 'GET', 'DELETE', 'HEAD']; +const ExponentialBackoffCeiling = 10; +const ExponentialBackoffTimeSlice = 5; +class HttpClientResponse { + constructor(message) { + this.message = message; + } + readBody() { + return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { + let buffer = Buffer.alloc(0); + const encodingCharset = util.obtainContentCharset(this); + // Extract Encoding from header: 'content-encoding' + // Match `gzip`, `gzip, deflate` variations of GZIP encoding + const contentEncoding = this.message.headers['content-encoding'] || ''; + const isGzippedEncoded = new RegExp('(gzip$)|(gzip, *deflate)').test(contentEncoding); + this.message.on('data', function (data) { + const chunk = (typeof data === 'string') ? Buffer.from(data, encodingCharset) : data; + buffer = Buffer.concat([buffer, chunk]); + }).on('end', function () { + return __awaiter(this, void 0, void 0, function* () { + if (isGzippedEncoded) { // Process GZipped Response Body HERE + const gunzippedBody = yield util.decompressGzippedContent(buffer, encodingCharset); + resolve(gunzippedBody); + } + resolve(buffer.toString(encodingCharset)); + }); + }).on('error', function (err) { + reject(err); + }); + })); + } +} +exports.HttpClientResponse = HttpClientResponse; +function isHttps(requestUrl) { + let parsedUrl = url.parse(requestUrl); + return parsedUrl.protocol === 'https:'; +} +exports.isHttps = isHttps; +var EnvironmentVariables; +(function (EnvironmentVariables) { + EnvironmentVariables["HTTP_PROXY"] = "HTTP_PROXY"; + EnvironmentVariables["HTTPS_PROXY"] = "HTTPS_PROXY"; + EnvironmentVariables["NO_PROXY"] = "NO_PROXY"; +})(EnvironmentVariables || (EnvironmentVariables = {})); +class HttpClient { + constructor(userAgent, handlers, requestOptions) { + this._ignoreSslError = false; + this._allowRedirects = true; + this._allowRedirectDowngrade = false; + this._maxRedirects = 50; + this._allowRetries = false; + this._maxRetries = 1; + this._keepAlive = false; + this._disposed = false; + this.userAgent = userAgent; + this.handlers = handlers || []; + let no_proxy = process.env[EnvironmentVariables.NO_PROXY]; + if (no_proxy) { + this._httpProxyBypassHosts = []; + no_proxy.split(',').forEach(bypass => { + this._httpProxyBypassHosts.push(new RegExp(bypass, 'i')); + }); + } + this.requestOptions = requestOptions; + if (requestOptions) { + if (requestOptions.ignoreSslError != null) { + this._ignoreSslError = requestOptions.ignoreSslError; + } + this._socketTimeout = requestOptions.socketTimeout; + this._httpProxy = requestOptions.proxy; + if (requestOptions.proxy && requestOptions.proxy.proxyBypassHosts) { + this._httpProxyBypassHosts = []; + requestOptions.proxy.proxyBypassHosts.forEach(bypass => { + this._httpProxyBypassHosts.push(new RegExp(bypass, 'i')); + }); + } + this._certConfig = requestOptions.cert; + if (this._certConfig) { + // If using cert, need fs + fs = __webpack_require__(747); + // cache the cert content into memory, so we don't have to read it from disk every time + if (this._certConfig.caFile && fs.existsSync(this._certConfig.caFile)) { + this._ca = fs.readFileSync(this._certConfig.caFile, 'utf8'); + } + if (this._certConfig.certFile && fs.existsSync(this._certConfig.certFile)) { + this._cert = fs.readFileSync(this._certConfig.certFile, 'utf8'); + } + if (this._certConfig.keyFile && fs.existsSync(this._certConfig.keyFile)) { + this._key = fs.readFileSync(this._certConfig.keyFile, 'utf8'); + } + } + if (requestOptions.allowRedirects != null) { + this._allowRedirects = requestOptions.allowRedirects; + } + if (requestOptions.allowRedirectDowngrade != null) { + this._allowRedirectDowngrade = requestOptions.allowRedirectDowngrade; + } + if (requestOptions.maxRedirects != null) { + this._maxRedirects = Math.max(requestOptions.maxRedirects, 0); + } + if (requestOptions.keepAlive != null) { + this._keepAlive = requestOptions.keepAlive; + } + if (requestOptions.allowRetries != null) { + this._allowRetries = requestOptions.allowRetries; + } + if (requestOptions.maxRetries != null) { + this._maxRetries = requestOptions.maxRetries; + } + } + } + options(requestUrl, additionalHeaders) { + return this.request('OPTIONS', requestUrl, null, additionalHeaders || {}); + } + get(requestUrl, additionalHeaders) { + return this.request('GET', requestUrl, null, additionalHeaders || {}); + } + del(requestUrl, additionalHeaders) { + return this.request('DELETE', requestUrl, null, additionalHeaders || {}); + } + post(requestUrl, data, additionalHeaders) { + return this.request('POST', requestUrl, data, additionalHeaders || {}); + } + patch(requestUrl, data, additionalHeaders) { + return this.request('PATCH', requestUrl, data, additionalHeaders || {}); + } + put(requestUrl, data, additionalHeaders) { + return this.request('PUT', requestUrl, data, additionalHeaders || {}); + } + head(requestUrl, additionalHeaders) { + return this.request('HEAD', requestUrl, null, additionalHeaders || {}); + } + sendStream(verb, requestUrl, stream, additionalHeaders) { + return this.request(verb, requestUrl, stream, additionalHeaders); + } + /** + * Makes a raw http request. + * All other methods such as get, post, patch, and request ultimately call this. + * Prefer get, del, post and patch + */ + request(verb, requestUrl, data, headers) { + return __awaiter(this, void 0, void 0, function* () { + if (this._disposed) { + throw new Error("Client has already been disposed."); + } + let parsedUrl = url.parse(requestUrl); + let info = this._prepareRequest(verb, parsedUrl, headers); + // Only perform retries on reads since writes may not be idempotent. + let maxTries = (this._allowRetries && RetryableHttpVerbs.indexOf(verb) != -1) ? this._maxRetries + 1 : 1; + let numTries = 0; + let response; + while (numTries < maxTries) { + response = yield this.requestRaw(info, data); + // Check if it's an authentication challenge + if (response && response.message && response.message.statusCode === HttpCodes.Unauthorized) { + let authenticationHandler; + for (let i = 0; i < this.handlers.length; i++) { + if (this.handlers[i].canHandleAuthentication(response)) { + authenticationHandler = this.handlers[i]; + break; + } + } + if (authenticationHandler) { + return authenticationHandler.handleAuthentication(this, info, data); + } + else { + // We have received an unauthorized response but have no handlers to handle it. + // Let the response return to the caller. + return response; + } + } + let redirectsRemaining = this._maxRedirects; + while (HttpRedirectCodes.indexOf(response.message.statusCode) != -1 + && this._allowRedirects + && redirectsRemaining > 0) { + const redirectUrl = response.message.headers["location"]; + if (!redirectUrl) { + // if there's no location to redirect to, we won't + break; + } + let parsedRedirectUrl = url.parse(redirectUrl); + if (parsedUrl.protocol == 'https:' && parsedUrl.protocol != parsedRedirectUrl.protocol && !this._allowRedirectDowngrade) { + throw new Error("Redirect from HTTPS to HTTP protocol. This downgrade is not allowed for security reasons. If you want to allow this behavior, set the allowRedirectDowngrade option to true."); + } + // we need to finish reading the response before reassigning response + // which will leak the open socket. + yield response.readBody(); + // let's make the request with the new redirectUrl + info = this._prepareRequest(verb, parsedRedirectUrl, headers); + response = yield this.requestRaw(info, data); + redirectsRemaining--; + } + if (HttpResponseRetryCodes.indexOf(response.message.statusCode) == -1) { + // If not a retry code, return immediately instead of retrying + return response; + } + numTries += 1; + if (numTries < maxTries) { + yield response.readBody(); + yield this._performExponentialBackoff(numTries); + } + } + return response; + }); + } + /** + * Needs to be called if keepAlive is set to true in request options. + */ + dispose() { + if (this._agent) { + this._agent.destroy(); + } + this._disposed = true; + } + /** + * Raw request. + * @param info + * @param data + */ + requestRaw(info, data) { + return new Promise((resolve, reject) => { + let callbackForResult = function (err, res) { + if (err) { + reject(err); + } + resolve(res); + }; + this.requestRawWithCallback(info, data, callbackForResult); + }); + } + /** + * Raw request with callback. + * @param info + * @param data + * @param onResult + */ + requestRawWithCallback(info, data, onResult) { + let socket; + let isDataString = typeof (data) === 'string'; + if (typeof (data) === 'string') { + info.options.headers["Content-Length"] = Buffer.byteLength(data, 'utf8'); + } + let callbackCalled = false; + let handleResult = (err, res) => { + if (!callbackCalled) { + callbackCalled = true; + onResult(err, res); + } + }; + let req = info.httpModule.request(info.options, (msg) => { + let res = new HttpClientResponse(msg); + handleResult(null, res); + }); + req.on('socket', (sock) => { + socket = sock; + }); + // If we ever get disconnected, we want the socket to timeout eventually + req.setTimeout(this._socketTimeout || 3 * 60000, () => { + if (socket) { + socket.end(); + } + handleResult(new Error('Request timeout: ' + info.options.path), null); + }); + req.on('error', function (err) { + // err has statusCode property + // res should have headers + handleResult(err, null); + }); + if (data && typeof (data) === 'string') { + req.write(data, 'utf8'); + } + if (data && typeof (data) !== 'string') { + data.on('close', function () { + req.end(); + }); + data.pipe(req); + } + else { + req.end(); + } + } + _prepareRequest(method, requestUrl, headers) { + const info = {}; + info.parsedUrl = requestUrl; + const usingSsl = info.parsedUrl.protocol === 'https:'; + info.httpModule = usingSsl ? https : http; + const defaultPort = usingSsl ? 443 : 80; + info.options = {}; + info.options.host = info.parsedUrl.hostname; + info.options.port = info.parsedUrl.port ? parseInt(info.parsedUrl.port) : defaultPort; + info.options.path = (info.parsedUrl.pathname || '') + (info.parsedUrl.search || ''); + info.options.method = method; + info.options.headers = this._mergeHeaders(headers); + if (this.userAgent != null) { + info.options.headers["user-agent"] = this.userAgent; + } + info.options.agent = this._getAgent(info.parsedUrl); + // gives handlers an opportunity to participate + if (this.handlers && !this._isPresigned(url.format(requestUrl))) { + this.handlers.forEach((handler) => { + handler.prepareRequest(info.options); + }); + } + return info; + } + _isPresigned(requestUrl) { + if (this.requestOptions && this.requestOptions.presignedUrlPatterns) { + const patterns = this.requestOptions.presignedUrlPatterns; + for (let i = 0; i < patterns.length; i++) { + if (requestUrl.match(patterns[i])) { + return true; + } + } + } + return false; + } + _mergeHeaders(headers) { + const lowercaseKeys = obj => Object.keys(obj).reduce((c, k) => (c[k.toLowerCase()] = obj[k], c), {}); + if (this.requestOptions && this.requestOptions.headers) { + return Object.assign({}, lowercaseKeys(this.requestOptions.headers), lowercaseKeys(headers)); + } + return lowercaseKeys(headers || {}); + } + _getAgent(parsedUrl) { + let agent; + let proxy = this._getProxy(parsedUrl); + let useProxy = proxy.proxyUrl && proxy.proxyUrl.hostname && !this._isMatchInBypassProxyList(parsedUrl); + if (this._keepAlive && useProxy) { + agent = this._proxyAgent; + } + if (this._keepAlive && !useProxy) { + agent = this._agent; + } + // if agent is already assigned use that agent. + if (!!agent) { + return agent; + } + const usingSsl = parsedUrl.protocol === 'https:'; + let maxSockets = 100; + if (!!this.requestOptions) { + maxSockets = this.requestOptions.maxSockets || http.globalAgent.maxSockets; + } + if (useProxy) { + // If using proxy, need tunnel + if (!tunnel) { + tunnel = __webpack_require__(413); + } + const agentOptions = { + maxSockets: maxSockets, + keepAlive: this._keepAlive, + proxy: { + proxyAuth: proxy.proxyAuth, + host: proxy.proxyUrl.hostname, + port: proxy.proxyUrl.port + }, + }; + let tunnelAgent; + const overHttps = proxy.proxyUrl.protocol === 'https:'; + if (usingSsl) { + tunnelAgent = overHttps ? tunnel.httpsOverHttps : tunnel.httpsOverHttp; + } + else { + tunnelAgent = overHttps ? tunnel.httpOverHttps : tunnel.httpOverHttp; + } + agent = tunnelAgent(agentOptions); + this._proxyAgent = agent; + } + // if reusing agent across request and tunneling agent isn't assigned create a new agent + if (this._keepAlive && !agent) { + const options = { keepAlive: this._keepAlive, maxSockets: maxSockets }; + agent = usingSsl ? new https.Agent(options) : new http.Agent(options); + this._agent = agent; + } + // if not using private agent and tunnel agent isn't setup then use global agent + if (!agent) { + agent = usingSsl ? https.globalAgent : http.globalAgent; + } + if (usingSsl && this._ignoreSslError) { + // we don't want to set NODE_TLS_REJECT_UNAUTHORIZED=0 since that will affect request for entire process + // http.RequestOptions doesn't expose a way to modify RequestOptions.agent.options + // we have to cast it to any and change it directly + agent.options = Object.assign(agent.options || {}, { rejectUnauthorized: false }); + } + if (usingSsl && this._certConfig) { + agent.options = Object.assign(agent.options || {}, { ca: this._ca, cert: this._cert, key: this._key, passphrase: this._certConfig.passphrase }); + } + return agent; + } + _getProxy(parsedUrl) { + let usingSsl = parsedUrl.protocol === 'https:'; + let proxyConfig = this._httpProxy; + // fallback to http_proxy and https_proxy env + let https_proxy = process.env[EnvironmentVariables.HTTPS_PROXY]; + let http_proxy = process.env[EnvironmentVariables.HTTP_PROXY]; + if (!proxyConfig) { + if (https_proxy && usingSsl) { + proxyConfig = { + proxyUrl: https_proxy + }; + } + else if (http_proxy) { + proxyConfig = { + proxyUrl: http_proxy + }; + } + } + let proxyUrl; + let proxyAuth; + if (proxyConfig) { + if (proxyConfig.proxyUrl.length > 0) { + proxyUrl = url.parse(proxyConfig.proxyUrl); + } + if (proxyConfig.proxyUsername || proxyConfig.proxyPassword) { + proxyAuth = proxyConfig.proxyUsername + ":" + proxyConfig.proxyPassword; + } + } + return { proxyUrl: proxyUrl, proxyAuth: proxyAuth }; + } + _isMatchInBypassProxyList(parsedUrl) { + if (!this._httpProxyBypassHosts) { + return false; + } + let bypass = false; + this._httpProxyBypassHosts.forEach(bypassHost => { + if (bypassHost.test(parsedUrl.href)) { + bypass = true; + } + }); + return bypass; + } + _performExponentialBackoff(retryNumber) { + retryNumber = Math.min(ExponentialBackoffCeiling, retryNumber); + const ms = ExponentialBackoffTimeSlice * Math.pow(2, retryNumber); + return new Promise(resolve => setTimeout(() => resolve(), ms)); + } +} +exports.HttpClient = HttpClient; -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); + +/***/ }), + +/***/ 897: +/***/ (function(module, __unusedexports, __webpack_require__) { + +"use strict"; + + +var utils = __webpack_require__(581); +var formats = __webpack_require__(13); +var has = Object.prototype.hasOwnProperty; + +var arrayPrefixGenerators = { + brackets: function brackets(prefix) { + return prefix + '[]'; + }, + comma: 'comma', + indices: function indices(prefix, key) { + return prefix + '[' + key + ']'; + }, + repeat: function repeat(prefix) { + return prefix; + } }; -Object.defineProperty(exports, "__esModule", { value: true }); -const url = __webpack_require__(835); -const http = __webpack_require__(605); -const https = __webpack_require__(211); -let fs; -let tunnel; -var HttpCodes; -(function (HttpCodes) { - HttpCodes[HttpCodes["OK"] = 200] = "OK"; - HttpCodes[HttpCodes["MultipleChoices"] = 300] = "MultipleChoices"; - HttpCodes[HttpCodes["MovedPermanently"] = 301] = "MovedPermanently"; - HttpCodes[HttpCodes["ResourceMoved"] = 302] = "ResourceMoved"; - HttpCodes[HttpCodes["SeeOther"] = 303] = "SeeOther"; - HttpCodes[HttpCodes["NotModified"] = 304] = "NotModified"; - HttpCodes[HttpCodes["UseProxy"] = 305] = "UseProxy"; - HttpCodes[HttpCodes["SwitchProxy"] = 306] = "SwitchProxy"; - HttpCodes[HttpCodes["TemporaryRedirect"] = 307] = "TemporaryRedirect"; - HttpCodes[HttpCodes["PermanentRedirect"] = 308] = "PermanentRedirect"; - HttpCodes[HttpCodes["BadRequest"] = 400] = "BadRequest"; - HttpCodes[HttpCodes["Unauthorized"] = 401] = "Unauthorized"; - HttpCodes[HttpCodes["PaymentRequired"] = 402] = "PaymentRequired"; - HttpCodes[HttpCodes["Forbidden"] = 403] = "Forbidden"; - HttpCodes[HttpCodes["NotFound"] = 404] = "NotFound"; - HttpCodes[HttpCodes["MethodNotAllowed"] = 405] = "MethodNotAllowed"; - HttpCodes[HttpCodes["NotAcceptable"] = 406] = "NotAcceptable"; - HttpCodes[HttpCodes["ProxyAuthenticationRequired"] = 407] = "ProxyAuthenticationRequired"; - HttpCodes[HttpCodes["RequestTimeout"] = 408] = "RequestTimeout"; - HttpCodes[HttpCodes["Conflict"] = 409] = "Conflict"; - HttpCodes[HttpCodes["Gone"] = 410] = "Gone"; - HttpCodes[HttpCodes["InternalServerError"] = 500] = "InternalServerError"; - HttpCodes[HttpCodes["NotImplemented"] = 501] = "NotImplemented"; - HttpCodes[HttpCodes["BadGateway"] = 502] = "BadGateway"; - HttpCodes[HttpCodes["ServiceUnavailable"] = 503] = "ServiceUnavailable"; - HttpCodes[HttpCodes["GatewayTimeout"] = 504] = "GatewayTimeout"; -})(HttpCodes = exports.HttpCodes || (exports.HttpCodes = {})); -const HttpRedirectCodes = [HttpCodes.MovedPermanently, HttpCodes.ResourceMoved, HttpCodes.SeeOther, HttpCodes.TemporaryRedirect, HttpCodes.PermanentRedirect]; -const HttpResponseRetryCodes = [HttpCodes.BadGateway, HttpCodes.ServiceUnavailable, HttpCodes.GatewayTimeout]; -const RetryableHttpVerbs = ['OPTIONS', 'GET', 'DELETE', 'HEAD']; -const ExponentialBackoffCeiling = 10; -const ExponentialBackoffTimeSlice = 5; -class HttpClientResponse { - constructor(message) { - this.message = message; + +var isArray = Array.isArray; +var push = Array.prototype.push; +var pushToArray = function (arr, valueOrArray) { + push.apply(arr, isArray(valueOrArray) ? valueOrArray : [valueOrArray]); +}; + +var toISO = Date.prototype.toISOString; + +var defaultFormat = formats['default']; +var defaults = { + addQueryPrefix: false, + allowDots: false, + charset: 'utf-8', + charsetSentinel: false, + delimiter: '&', + encode: true, + encoder: utils.encode, + encodeValuesOnly: false, + format: defaultFormat, + formatter: formats.formatters[defaultFormat], + // deprecated + indices: false, + serializeDate: function serializeDate(date) { + return toISO.call(date); + }, + skipNulls: false, + strictNullHandling: false +}; + +var isNonNullishPrimitive = function isNonNullishPrimitive(v) { + return typeof v === 'string' + || typeof v === 'number' + || typeof v === 'boolean' + || typeof v === 'symbol' + || typeof v === 'bigint'; +}; + +var stringify = function stringify( + object, + prefix, + generateArrayPrefix, + strictNullHandling, + skipNulls, + encoder, + filter, + sort, + allowDots, + serializeDate, + formatter, + encodeValuesOnly, + charset +) { + var obj = object; + if (typeof filter === 'function') { + obj = filter(prefix, obj); + } else if (obj instanceof Date) { + obj = serializeDate(obj); + } else if (generateArrayPrefix === 'comma' && isArray(obj)) { + obj = obj.join(','); } - readBody() { - return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { - let output = ''; - this.message.on('data', (chunk) => { - output += chunk; - }); - this.message.on('end', () => { - resolve(output); - }); - })); + + if (obj === null) { + if (strictNullHandling) { + return encoder && !encodeValuesOnly ? encoder(prefix, defaults.encoder, charset, 'key') : prefix; + } + + obj = ''; } -} -exports.HttpClientResponse = HttpClientResponse; -function isHttps(requestUrl) { - let parsedUrl = url.parse(requestUrl); - return parsedUrl.protocol === 'https:'; -} -exports.isHttps = isHttps; -var EnvironmentVariables; -(function (EnvironmentVariables) { - EnvironmentVariables["HTTP_PROXY"] = "HTTP_PROXY"; - EnvironmentVariables["HTTPS_PROXY"] = "HTTPS_PROXY"; -})(EnvironmentVariables || (EnvironmentVariables = {})); -class HttpClient { - constructor(userAgent, handlers, requestOptions) { - this._ignoreSslError = false; - this._allowRedirects = true; - this._maxRedirects = 50; - this._allowRetries = false; - this._maxRetries = 1; - this._keepAlive = false; - this._disposed = false; - this.userAgent = userAgent; - this.handlers = handlers || []; - this.requestOptions = requestOptions; - if (requestOptions) { - if (requestOptions.ignoreSslError != null) { - this._ignoreSslError = requestOptions.ignoreSslError; - } - this._socketTimeout = requestOptions.socketTimeout; - this._httpProxy = requestOptions.proxy; - if (requestOptions.proxy && requestOptions.proxy.proxyBypassHosts) { - this._httpProxyBypassHosts = []; - requestOptions.proxy.proxyBypassHosts.forEach(bypass => { - this._httpProxyBypassHosts.push(new RegExp(bypass, 'i')); - }); - } - this._certConfig = requestOptions.cert; - if (this._certConfig) { - // If using cert, need fs - fs = __webpack_require__(747); - // cache the cert content into memory, so we don't have to read it from disk every time - if (this._certConfig.caFile && fs.existsSync(this._certConfig.caFile)) { - this._ca = fs.readFileSync(this._certConfig.caFile, 'utf8'); - } - if (this._certConfig.certFile && fs.existsSync(this._certConfig.certFile)) { - this._cert = fs.readFileSync(this._certConfig.certFile, 'utf8'); - } - if (this._certConfig.keyFile && fs.existsSync(this._certConfig.keyFile)) { - this._key = fs.readFileSync(this._certConfig.keyFile, 'utf8'); - } - } - if (requestOptions.allowRedirects != null) { - this._allowRedirects = requestOptions.allowRedirects; - } - if (requestOptions.maxRedirects != null) { - this._maxRedirects = Math.max(requestOptions.maxRedirects, 0); - } - if (requestOptions.keepAlive != null) { - this._keepAlive = requestOptions.keepAlive; - } - if (requestOptions.allowRetries != null) { - this._allowRetries = requestOptions.allowRetries; - } - if (requestOptions.maxRetries != null) { - this._maxRetries = requestOptions.maxRetries; - } + + if (isNonNullishPrimitive(obj) || utils.isBuffer(obj)) { + if (encoder) { + var keyValue = encodeValuesOnly ? prefix : encoder(prefix, defaults.encoder, charset, 'key'); + return [formatter(keyValue) + '=' + formatter(encoder(obj, defaults.encoder, charset, 'value'))]; + } + return [formatter(prefix) + '=' + formatter(String(obj))]; + } + + var values = []; + + if (typeof obj === 'undefined') { + return values; + } + + var objKeys; + if (isArray(filter)) { + objKeys = filter; + } else { + var keys = Object.keys(obj); + objKeys = sort ? keys.sort(sort) : keys; + } + + for (var i = 0; i < objKeys.length; ++i) { + var key = objKeys[i]; + + if (skipNulls && obj[key] === null) { + continue; + } + + if (isArray(obj)) { + pushToArray(values, stringify( + obj[key], + typeof generateArrayPrefix === 'function' ? generateArrayPrefix(prefix, key) : prefix, + generateArrayPrefix, + strictNullHandling, + skipNulls, + encoder, + filter, + sort, + allowDots, + serializeDate, + formatter, + encodeValuesOnly, + charset + )); + } else { + pushToArray(values, stringify( + obj[key], + prefix + (allowDots ? '.' + key : '[' + key + ']'), + generateArrayPrefix, + strictNullHandling, + skipNulls, + encoder, + filter, + sort, + allowDots, + serializeDate, + formatter, + encodeValuesOnly, + charset + )); } } - options(requestUrl, additionalHeaders) { - return this.request('OPTIONS', requestUrl, null, additionalHeaders || {}); + + return values; +}; + +var normalizeStringifyOptions = function normalizeStringifyOptions(opts) { + if (!opts) { + return defaults; } - get(requestUrl, additionalHeaders) { - return this.request('GET', requestUrl, null, additionalHeaders || {}); + + if (opts.encoder !== null && opts.encoder !== undefined && typeof opts.encoder !== 'function') { + throw new TypeError('Encoder has to be a function.'); } - del(requestUrl, additionalHeaders) { - return this.request('DELETE', requestUrl, null, additionalHeaders || {}); + + var charset = opts.charset || defaults.charset; + if (typeof opts.charset !== 'undefined' && opts.charset !== 'utf-8' && opts.charset !== 'iso-8859-1') { + throw new TypeError('The charset option must be either utf-8, iso-8859-1, or undefined'); } - post(requestUrl, data, additionalHeaders) { - return this.request('POST', requestUrl, data, additionalHeaders || {}); - } - patch(requestUrl, data, additionalHeaders) { - return this.request('PATCH', requestUrl, data, additionalHeaders || {}); - } - put(requestUrl, data, additionalHeaders) { - return this.request('PUT', requestUrl, data, additionalHeaders || {}); - } - head(requestUrl, additionalHeaders) { - return this.request('HEAD', requestUrl, null, additionalHeaders || {}); - } - sendStream(verb, requestUrl, stream, additionalHeaders) { - return this.request(verb, requestUrl, stream, additionalHeaders); - } - /** - * Makes a raw http request. - * All other methods such as get, post, patch, and request ultimately call this. - * Prefer get, del, post and patch - */ - request(verb, requestUrl, data, headers) { - return __awaiter(this, void 0, void 0, function* () { - if (this._disposed) { - throw new Error("Client has already been disposed."); - } - let info = this._prepareRequest(verb, requestUrl, headers); - // Only perform retries on reads since writes may not be idempotent. - let maxTries = (this._allowRetries && RetryableHttpVerbs.indexOf(verb) != -1) ? this._maxRetries + 1 : 1; - let numTries = 0; - let response; - while (numTries < maxTries) { - response = yield this.requestRaw(info, data); - // Check if it's an authentication challenge - if (response && response.message && response.message.statusCode === HttpCodes.Unauthorized) { - let authenticationHandler; - for (let i = 0; i < this.handlers.length; i++) { - if (this.handlers[i].canHandleAuthentication(response)) { - authenticationHandler = this.handlers[i]; - break; - } - } - if (authenticationHandler) { - return authenticationHandler.handleAuthentication(this, info, data); - } - else { - // We have received an unauthorized response but have no handlers to handle it. - // Let the response return to the caller. - return response; - } - } - let redirectsRemaining = this._maxRedirects; - while (HttpRedirectCodes.indexOf(response.message.statusCode) != -1 - && this._allowRedirects - && redirectsRemaining > 0) { - const redirectUrl = response.message.headers["location"]; - if (!redirectUrl) { - // if there's no location to redirect to, we won't - break; - } - // we need to finish reading the response before reassigning response - // which will leak the open socket. - yield response.readBody(); - // let's make the request with the new redirectUrl - info = this._prepareRequest(verb, redirectUrl, headers); - response = yield this.requestRaw(info, data); - redirectsRemaining--; - } - if (HttpResponseRetryCodes.indexOf(response.message.statusCode) == -1) { - // If not a retry code, return immediately instead of retrying - return response; - } - numTries += 1; - if (numTries < maxTries) { - yield response.readBody(); - yield this._performExponentialBackoff(numTries); - } - } - return response; - }); - } - /** - * Needs to be called if keepAlive is set to true in request options. - */ - dispose() { - if (this._agent) { - this._agent.destroy(); + + var format = formats['default']; + if (typeof opts.format !== 'undefined') { + if (!has.call(formats.formatters, opts.format)) { + throw new TypeError('Unknown format option provided.'); } - this._disposed = true; + format = opts.format; } - /** - * Raw request. - * @param info - * @param data - */ - requestRaw(info, data) { - return new Promise((resolve, reject) => { - let callbackForResult = function (err, res) { - if (err) { - reject(err); - } - resolve(res); - }; - this.requestRawWithCallback(info, data, callbackForResult); - }); + var formatter = formats.formatters[format]; + + var filter = defaults.filter; + if (typeof opts.filter === 'function' || isArray(opts.filter)) { + filter = opts.filter; } - /** - * Raw request with callback. - * @param info - * @param data - * @param onResult - */ - requestRawWithCallback(info, data, onResult) { - let socket; - let isDataString = typeof (data) === 'string'; - if (typeof (data) === 'string') { - info.options.headers["Content-Length"] = Buffer.byteLength(data, 'utf8'); + + return { + addQueryPrefix: typeof opts.addQueryPrefix === 'boolean' ? opts.addQueryPrefix : defaults.addQueryPrefix, + allowDots: typeof opts.allowDots === 'undefined' ? defaults.allowDots : !!opts.allowDots, + charset: charset, + charsetSentinel: typeof opts.charsetSentinel === 'boolean' ? opts.charsetSentinel : defaults.charsetSentinel, + delimiter: typeof opts.delimiter === 'undefined' ? defaults.delimiter : opts.delimiter, + encode: typeof opts.encode === 'boolean' ? opts.encode : defaults.encode, + encoder: typeof opts.encoder === 'function' ? opts.encoder : defaults.encoder, + encodeValuesOnly: typeof opts.encodeValuesOnly === 'boolean' ? opts.encodeValuesOnly : defaults.encodeValuesOnly, + filter: filter, + formatter: formatter, + serializeDate: typeof opts.serializeDate === 'function' ? opts.serializeDate : defaults.serializeDate, + skipNulls: typeof opts.skipNulls === 'boolean' ? opts.skipNulls : defaults.skipNulls, + sort: typeof opts.sort === 'function' ? opts.sort : null, + strictNullHandling: typeof opts.strictNullHandling === 'boolean' ? opts.strictNullHandling : defaults.strictNullHandling + }; +}; + +module.exports = function (object, opts) { + var obj = object; + var options = normalizeStringifyOptions(opts); + + var objKeys; + var filter; + + if (typeof options.filter === 'function') { + filter = options.filter; + obj = filter('', obj); + } else if (isArray(options.filter)) { + filter = options.filter; + objKeys = filter; + } + + var keys = []; + + if (typeof obj !== 'object' || obj === null) { + return ''; + } + + var arrayFormat; + if (opts && opts.arrayFormat in arrayPrefixGenerators) { + arrayFormat = opts.arrayFormat; + } else if (opts && 'indices' in opts) { + arrayFormat = opts.indices ? 'indices' : 'repeat'; + } else { + arrayFormat = 'indices'; + } + + var generateArrayPrefix = arrayPrefixGenerators[arrayFormat]; + + if (!objKeys) { + objKeys = Object.keys(obj); + } + + if (options.sort) { + objKeys.sort(options.sort); + } + + for (var i = 0; i < objKeys.length; ++i) { + var key = objKeys[i]; + + if (options.skipNulls && obj[key] === null) { + continue; } - let callbackCalled = false; - let handleResult = (err, res) => { - if (!callbackCalled) { - callbackCalled = true; - onResult(err, res); - } - }; - let req = info.httpModule.request(info.options, (msg) => { - let res = new HttpClientResponse(msg); - handleResult(null, res); - }); - req.on('socket', (sock) => { - socket = sock; - }); - // If we ever get disconnected, we want the socket to timeout eventually - req.setTimeout(this._socketTimeout || 3 * 60000, () => { - if (socket) { - socket.end(); - } - handleResult(new Error('Request timeout: ' + info.options.path), null); - }); - req.on('error', function (err) { - // err has statusCode property - // res should have headers - handleResult(err, null); - }); - if (data && typeof (data) === 'string') { - req.write(data, 'utf8'); - } - if (data && typeof (data) !== 'string') { - data.on('close', function () { - req.end(); - }); - data.pipe(req); - } - else { - req.end(); + pushToArray(keys, stringify( + obj[key], + key, + generateArrayPrefix, + options.strictNullHandling, + options.skipNulls, + options.encode ? options.encoder : null, + options.filter, + options.sort, + options.allowDots, + options.serializeDate, + options.formatter, + options.encodeValuesOnly, + options.charset + )); + } + + var joined = keys.join(options.delimiter); + var prefix = options.addQueryPrefix === true ? '?' : ''; + + if (options.charsetSentinel) { + if (options.charset === 'iso-8859-1') { + // encodeURIComponent('✓'), the "numeric entity" representation of a checkmark + prefix += 'utf8=%26%2310003%3B&'; + } else { + // encodeURIComponent('✓') + prefix += 'utf8=%E2%9C%93&'; } } - _prepareRequest(method, requestUrl, headers) { - const info = {}; - info.parsedUrl = url.parse(requestUrl); - const usingSsl = info.parsedUrl.protocol === 'https:'; - info.httpModule = usingSsl ? https : http; - const defaultPort = usingSsl ? 443 : 80; - info.options = {}; - info.options.host = info.parsedUrl.hostname; - info.options.port = info.parsedUrl.port ? parseInt(info.parsedUrl.port) : defaultPort; - info.options.path = (info.parsedUrl.pathname || '') + (info.parsedUrl.search || ''); - info.options.method = method; - info.options.headers = this._mergeHeaders(headers); - info.options.headers["user-agent"] = this.userAgent; - info.options.agent = this._getAgent(requestUrl); - // gives handlers an opportunity to participate - if (this.handlers && !this._isPresigned(requestUrl)) { - this.handlers.forEach((handler) => { - handler.prepareRequest(info.options); - }); - } - return info; - } - _isPresigned(requestUrl) { - if (this.requestOptions && this.requestOptions.presignedUrlPatterns) { - const patterns = this.requestOptions.presignedUrlPatterns; - for (let i = 0; i < patterns.length; i++) { - if (requestUrl.match(patterns[i])) { - return true; - } - } - } - return false; - } - _mergeHeaders(headers) { - const lowercaseKeys = obj => Object.keys(obj).reduce((c, k) => (c[k.toLowerCase()] = obj[k], c), {}); - if (this.requestOptions && this.requestOptions.headers) { - return Object.assign({}, lowercaseKeys(this.requestOptions.headers), lowercaseKeys(headers)); - } - return lowercaseKeys(headers || {}); - } - _getAgent(requestUrl) { - let agent; - let proxy = this._getProxy(requestUrl); - let useProxy = proxy.proxyUrl && proxy.proxyUrl.hostname && !this._isBypassProxy(requestUrl); - if (this._keepAlive && useProxy) { - agent = this._proxyAgent; - } - if (this._keepAlive && !useProxy) { - agent = this._agent; - } - // if agent is already assigned use that agent. - if (!!agent) { - return agent; - } - let parsedUrl = url.parse(requestUrl); - const usingSsl = parsedUrl.protocol === 'https:'; - let maxSockets = 100; - if (!!this.requestOptions) { - maxSockets = this.requestOptions.maxSockets || http.globalAgent.maxSockets; - } - if (useProxy) { - // If using proxy, need tunnel - if (!tunnel) { - tunnel = __webpack_require__(413); - } - const agentOptions = { - maxSockets: maxSockets, - keepAlive: this._keepAlive, - proxy: { - proxyAuth: proxy.proxyAuth, - host: proxy.proxyUrl.hostname, - port: proxy.proxyUrl.port - }, - }; - let tunnelAgent; - const overHttps = proxy.proxyUrl.protocol === 'https:'; - if (usingSsl) { - tunnelAgent = overHttps ? tunnel.httpsOverHttps : tunnel.httpsOverHttp; - } - else { - tunnelAgent = overHttps ? tunnel.httpOverHttps : tunnel.httpOverHttp; - } - agent = tunnelAgent(agentOptions); - this._proxyAgent = agent; - } - // if reusing agent across request and tunneling agent isn't assigned create a new agent - if (this._keepAlive && !agent) { - const options = { keepAlive: this._keepAlive, maxSockets: maxSockets }; - agent = usingSsl ? new https.Agent(options) : new http.Agent(options); - this._agent = agent; - } - // if not using private agent and tunnel agent isn't setup then use global agent - if (!agent) { - agent = usingSsl ? https.globalAgent : http.globalAgent; - } - if (usingSsl && this._ignoreSslError) { - // we don't want to set NODE_TLS_REJECT_UNAUTHORIZED=0 since that will affect request for entire process - // http.RequestOptions doesn't expose a way to modify RequestOptions.agent.options - // we have to cast it to any and change it directly - agent.options = Object.assign(agent.options || {}, { rejectUnauthorized: false }); - } - if (usingSsl && this._certConfig) { - agent.options = Object.assign(agent.options || {}, { ca: this._ca, cert: this._cert, key: this._key, passphrase: this._certConfig.passphrase }); - } - return agent; - } - _getProxy(requestUrl) { - const parsedUrl = url.parse(requestUrl); - let usingSsl = parsedUrl.protocol === 'https:'; - let proxyConfig = this._httpProxy; - // fallback to http_proxy and https_proxy env - let https_proxy = process.env[EnvironmentVariables.HTTPS_PROXY]; - let http_proxy = process.env[EnvironmentVariables.HTTP_PROXY]; - if (!proxyConfig) { - if (https_proxy && usingSsl) { - proxyConfig = { - proxyUrl: https_proxy - }; - } - else if (http_proxy) { - proxyConfig = { - proxyUrl: http_proxy - }; - } - } - let proxyUrl; - let proxyAuth; - if (proxyConfig) { - if (proxyConfig.proxyUrl.length > 0) { - proxyUrl = url.parse(proxyConfig.proxyUrl); - } - if (proxyConfig.proxyUsername || proxyConfig.proxyPassword) { - proxyAuth = proxyConfig.proxyUsername + ":" + proxyConfig.proxyPassword; - } - } - return { proxyUrl: proxyUrl, proxyAuth: proxyAuth }; - } - _isBypassProxy(requestUrl) { - if (!this._httpProxyBypassHosts) { - return false; - } - let bypass = false; - this._httpProxyBypassHosts.forEach(bypassHost => { - if (bypassHost.test(requestUrl)) { - bypass = true; - } - }); - return bypass; - } - _performExponentialBackoff(retryNumber) { - retryNumber = Math.min(ExponentialBackoffCeiling, retryNumber); - const ms = ExponentialBackoffTimeSlice * Math.pow(2, retryNumber); - return new Promise(resolve => setTimeout(() => resolve(), ms)); - } -} -exports.HttpClient = HttpClient; + + return joined.length > 0 ? prefix + joined : ''; +}; /***/ }), diff --git a/package-lock.json b/package-lock.json index 89d5da6..62afb32 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "create-pull-request", - "version": "1.8.0", + "version": "2.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -10,9 +10,12 @@ "integrity": "sha512-ZKdyhlSlyz38S6YFfPnyNgCDZuAF2T0Qv5eHflNWytPS8Qjvz39bZFMry9Bb/dpSnqWcNeav5yM2CTYpJeY+Dw==" }, "@actions/exec": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.0.1.tgz", - "integrity": "sha512-nvFkxwiicvpzNiCBF4wFBDfnBvi7xp/as7LE1hBxBxKG2L29+gkIPBiLKMVORL+Hg3JNf07AKRfl0V5djoypjQ==" + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.0.2.tgz", + "integrity": "sha512-Yo/wfcFuxbVjAaAfvx3aGLhMEuonOahas2jf8BwyA52IkXTAmLi7YVZTpGAQG/lTxuGoNLg9slTWQD4rr7rMDQ==", + "requires": { + "@actions/io": "^1.0.1" + } }, "@actions/io": { "version": "1.0.1", @@ -38,6 +41,11 @@ "integrity": "sha512-XU6uzwvv95DqxciQx+aOLhbyBx/13ky+RK1y88Age9Du3BlA4mMPCy13BGjayOrrumOzlq1XV3SD/BWiZENXlw==", "dev": true }, + "qs": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.1.tgz", + "integrity": "sha512-Cxm7/SS/y/Z3MHWSxXb8lIFqgqBowP5JMlTUFyJN88y0SGQhVmZnqFK/PeuMX9LzUyWsqqhNxIyg0jlzq946yA==" + }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", @@ -49,10 +57,11 @@ "integrity": "sha1-LTeFoVjBdMmhbcLARuxfxfF0IhM=" }, "typed-rest-client": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.5.0.tgz", - "integrity": "sha512-DVZRlmsfnTjp6ZJaatcdyvvwYwbWvR4YDNFDqb+qdTxpvaVP99YCpBkA8rxsLtAPjBVoDe4fNsnMIdZTiPuKWg==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.7.1.tgz", + "integrity": "sha512-fZRDWFtUp3J2E0jOiCJYZ9LDrYZHpjY95su//ekqXERS7C1qojP6movh7M4JGURJnBuTVsO0g2N4vEoW5o3Djw==", "requires": { + "qs": "^6.9.1", "tunnel": "0.0.4", "underscore": "1.8.3" } From 699d304845e1e83d16c89b03ef6a4516e243ef77 Mon Sep 17 00:00:00 2001 From: Peter Evans Date: Fri, 3 Jan 2020 14:18:10 +0900 Subject: [PATCH 30/30] Update docs to v2 --- README.md | 10 +++++----- docs/examples.md | 14 +++++++------- docs/updating.md | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 190a30e..4682561 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Create Pull Request action will: ```yml - name: Create Pull Request - uses: peter-evans/create-pull-request@v2-beta + uses: peter-evans/create-pull-request@v2 with: token: ${{ secrets.GITHUB_TOKEN }} ``` @@ -66,7 +66,7 @@ Note that in order to read the step output the action step must have an id. ```yml - name: Create Pull Request id: cpr - uses: peter-evans/create-pull-request@v2-beta + uses: peter-evans/create-pull-request@v2 with: token: ${{ secrets.GITHUB_TOKEN }} - name: Check outputs @@ -116,7 +116,7 @@ If neither `committer` or `author` inputs are supplied the action will default t In most cases, where the committer and author are the same, just the committer can be set. ```yml - name: Create Pull Request - uses: peter-evans/create-pull-request@v2-beta + uses: peter-evans/create-pull-request@v2 with: token: ${{ secrets.GITHUB_TOKEN }} committer: Peter Evans @@ -141,7 +141,7 @@ As well as relying on the action to handle uncommitted changes, you can addition - name: Uncommitted change run: date +%s > report.txt - name: Create Pull Request - uses: peter-evans/create-pull-request@v2-beta + uses: peter-evans/create-pull-request@v2 with: token: ${{ secrets.GITHUB_TOKEN }} ``` @@ -164,7 +164,7 @@ jobs: run: date +%s > report.txt - name: Create Pull Request id: cpr - uses: peter-evans/create-pull-request@v2-beta + uses: peter-evans/create-pull-request@v2 with: token: ${{ secrets.GITHUB_TOKEN }} commit-message: Add report file diff --git a/docs/examples.md b/docs/examples.md index 7dc9a37..84ed817 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -42,7 +42,7 @@ jobs: ncu -u npm install - name: Create Pull Request - uses: peter-evans/create-pull-request@v2-beta + uses: peter-evans/create-pull-request@v2 with: token: ${{ secrets.GITHUB_TOKEN }} commit-message: update dependencies @@ -75,7 +75,7 @@ jobs: - run: echo "##[set-output name=pr_title;]update to latest Go release ${{ steps.ensure_go.outputs.go_version}}" id: pr_title_maker - name: Create pull request - uses: peter-evans/create-pull-request@v2-beta + uses: peter-evans/create-pull-request@v2 with: token: ${{ secrets.GITHUB_TOKEN }} title: ${{ steps.pr_title_maker.outputs.pr_title }} @@ -126,7 +126,7 @@ jobs: # Update current release echo ${{ steps.swagger-ui.outputs.release_tag }} > swagger-ui.version - name: Create Pull Request - uses: peter-evans/create-pull-request@v2-beta + uses: peter-evans/create-pull-request@v2 with: token: ${{ secrets.GITHUB_TOKEN }} commit-message: Update swagger-ui to ${{ steps.swagger-ui.outputs.release_tag }} @@ -169,7 +169,7 @@ jobs: --domains quotes.toscrape.com \ http://quotes.toscrape.com/ - name: Create Pull Request - uses: peter-evans/create-pull-request@v2-beta + uses: peter-evans/create-pull-request@v2 with: token: ${{ secrets.GITHUB_TOKEN }} commit-message: update local website copy @@ -264,7 +264,7 @@ jobs: run: echo ::set-output name=branch-name::"autopep8-patches/${{ github.head_ref }}" - name: Create Pull Request if: steps.autopep8.outputs.exit-code == 2 - uses: peter-evans/create-pull-request@v2-beta + uses: peter-evans/create-pull-request@v2 with: token: ${{ secrets.GITHUB_TOKEN }} commit-message: autopep8 action fixes @@ -323,7 +323,7 @@ The recommended method is to use [`set-output`](https://help.github.com/en/githu echo ::set-output name=pr_body::"This PR was auto-generated on $(date +%d-%m-%Y) \ by [create-pull-request](https://github.com/peter-evans/create-pull-request)." - name: Create Pull Request - uses: peter-evans/create-pull-request@v2-beta + uses: peter-evans/create-pull-request@v2 with: token: ${{ secrets.GITHUB_TOKEN }} title: ${{ steps.vars.outputs.pr_title }} @@ -339,7 +339,7 @@ Alternatively, [`set-env`](https://help.github.com/en/github/automating-your-wor echo ::set-env name=PULL_REQUEST_BODY::"This PR was auto-generated on $(date +%d-%m-%Y) \ by [create-pull-request](https://github.com/peter-evans/create-pull-request)." - name: Create Pull Request - uses: peter-evans/create-pull-request@v2-beta + uses: peter-evans/create-pull-request@v2 with: token: ${{ secrets.GITHUB_TOKEN }} title: ${{ env.PULL_REQUEST_TITLE }} diff --git a/docs/updating.md b/docs/updating.md index 6ae1ec2..5f53bd0 100644 --- a/docs/updating.md +++ b/docs/updating.md @@ -20,6 +20,6 @@ ## New features -- Unpushed commits made during the workflow before the action runs will now be considered as changes to be raised in the pull request. See [Controlling commits](https://github.com/peter-evans/create-pull-request/tree/v2-beta#controlling-commits) for details. +- Unpushed commits made during the workflow before the action runs will now be considered as changes to be raised in the pull request. See [Controlling commits](https://github.com/peter-evans/create-pull-request#controlling-commits) for details. - New commits made to the pull request base will now be taken into account when pull requests are updated. - If an updated pull request no longer differs from its base it will automatically be closed and the pull request branch deleted.