Add feature to create project card for pull request

This commit is contained in:
Peter Evans 2019-11-24 08:48:32 +09:00
parent 823751817d
commit 1d1fedd99c
8 changed files with 308 additions and 186 deletions

View file

@ -36,6 +36,8 @@ jobs:
assignees: peter-evans assignees: peter-evans
reviewers: peter-evans reviewers: peter-evans
milestone: 1 milestone: 1
project: Example Project
project-column: To do
branch: example-patches branch: example-patches
branch-suffix: random branch-suffix: random
- name: Check outputs - name: Check outputs

View file

@ -28,6 +28,8 @@ jobs:
assignees: peter-evans assignees: peter-evans
reviewers: peter-evans reviewers: peter-evans
milestone: 1 milestone: 1
project: Example Project
project-column: To do
branch: example-patches branch: example-patches
branch-suffix: short-commit-hash branch-suffix: short-commit-hash
- name: Check outputs - name: Check outputs

View file

@ -24,6 +24,10 @@ inputs:
description: 'A comma separated list of GitHub teams to request a review from.' description: 'A comma separated list of GitHub teams to request a review from.'
milestone: milestone:
description: 'The number of the milestone to associate this pull request with.' description: 'The number of the milestone to associate this pull request with.'
project:
description: 'The name of the project for which a card should be created for this pull request.'
project-column:
description: 'The name of the project column under which a card should be created for this pull request.'
branch: branch:
description: 'The pull request branch name.' description: 'The pull request branch name.'
base: base:

4
dist/index.js vendored
View file

@ -987,6 +987,8 @@ async function run() {
reviewers: core.getInput("reviewers"), reviewers: core.getInput("reviewers"),
teamReviewers: core.getInput("team-reviewers"), teamReviewers: core.getInput("team-reviewers"),
milestone: core.getInput("milestone"), milestone: core.getInput("milestone"),
project: core.getInput("project"),
projectColumn: core.getInput("project-column"),
branch: core.getInput("branch"), branch: core.getInput("branch"),
base: core.getInput("base"), base: core.getInput("base"),
branchSuffix: core.getInput("branch-suffix"), branchSuffix: core.getInput("branch-suffix"),
@ -1006,6 +1008,8 @@ async function run() {
if (inputs.reviewers) process.env.PULL_REQUEST_REVIEWERS = inputs.reviewers; if (inputs.reviewers) process.env.PULL_REQUEST_REVIEWERS = inputs.reviewers;
if (inputs.teamReviewers) process.env.PULL_REQUEST_TEAM_REVIEWERS = inputs.teamReviewers; if (inputs.teamReviewers) process.env.PULL_REQUEST_TEAM_REVIEWERS = inputs.teamReviewers;
if (inputs.milestone) process.env.PULL_REQUEST_MILESTONE = inputs.milestone; 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.branch) process.env.PULL_REQUEST_BRANCH = inputs.branch;
if (inputs.base) process.env.PULL_REQUEST_BASE = inputs.base; if (inputs.base) process.env.PULL_REQUEST_BASE = inputs.base;
if (inputs.branchSuffix) process.env.BRANCH_SUFFIX = inputs.branchSuffix; if (inputs.branchSuffix) process.env.BRANCH_SUFFIX = inputs.branchSuffix;

View file

@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
''' Create Pull Request ''' """ Create Pull Request """
import json import json
import os import os
import random import random
@ -13,18 +13,18 @@ from github import Github, GithubException
def get_github_event(github_event_path): def get_github_event(github_event_path):
with open(github_event_path) as f: with open(github_event_path) as f:
github_event = json.load(f) github_event = json.load(f)
if bool(os.environ.get('DEBUG_EVENT')): if bool(os.environ.get("DEBUG_EVENT")):
print(os.environ['GITHUB_EVENT_NAME']) print(os.environ["GITHUB_EVENT_NAME"])
print(json.dumps(github_event, sort_keys=True, indent=2)) print(json.dumps(github_event, sort_keys=True, indent=2))
return github_event return github_event
def get_head_short_sha1(repo): def get_head_short_sha1(repo):
return repo.git.rev_parse('--short', 'HEAD') return repo.git.rev_parse("--short", "HEAD")
def get_random_suffix(size=7, chars=string.ascii_lowercase + string.digits): def get_random_suffix(size=7, chars=string.ascii_lowercase + string.digits):
return ''.join(random.choice(chars) for _ in range(size)) return "".join(random.choice(chars) for _ in range(size))
def remote_branch_exists(repo, branch): def remote_branch_exists(repo, branch):
@ -39,68 +39,105 @@ def get_author_default(event_name, event_data):
email = "{head_commit[author][email]}".format(**event_data) email = "{head_commit[author][email]}".format(**event_data)
name = "{head_commit[author][name]}".format(**event_data) name = "{head_commit[author][name]}".format(**event_data)
else: else:
email = os.environ['GITHUB_ACTOR'] + '@users.noreply.github.com' email = os.environ["GITHUB_ACTOR"] + "@users.noreply.github.com"
name = os.environ['GITHUB_ACTOR'] name = os.environ["GITHUB_ACTOR"]
return email, name return email, name
def set_git_config(git, email, name): def set_git_config(git, email, name):
print("Configuring git user as '%s <%s>'" % (name, email)) print("Configuring git user as '%s <%s>'" % (name, email))
git.config('--global', 'user.email', '"%s"' % email) git.config("--global", "user.email", '"%s"' % email)
git.config('--global', 'user.name', '"%s"' % name) git.config("--global", "user.name", '"%s"' % name)
def set_git_remote_url(git, token, github_repository): def set_git_remote_url(git, token, github_repository):
git.remote( git.remote(
'set-url', 'origin', "https://x-access-token:%s@github.com/%s" % "set-url",
(token, github_repository)) "origin",
"https://x-access-token:%s@github.com/%s" % (token, github_repository),
)
def checkout_branch(git, remote_exists, branch): def checkout_branch(git, remote_exists, branch):
if remote_exists: if remote_exists:
print("Checking out branch '%s'" % branch) print("Checking out branch '%s'" % branch)
git.stash('--include-untracked') git.stash("--include-untracked")
git.checkout(branch) git.checkout(branch)
try: try:
git.stash('pop') git.stash("pop")
except BaseException: except BaseException:
git.checkout('--theirs', '.') git.checkout("--theirs", ".")
git.reset() git.reset()
else: else:
print("Creating new branch '%s'" % branch) print("Creating new branch '%s'" % branch)
git.checkout('HEAD', b=branch) git.checkout("HEAD", b=branch)
def push_changes(git, branch, commit_message): def push_changes(git, branch, commit_message):
git.add('-A') git.add("-A")
git.commit(m=commit_message) git.commit(m=commit_message)
return git.push('-f', '--set-upstream', 'origin', branch) return git.push("-f", "--set-upstream", "origin", branch)
def cs_string_to_list(str): def cs_string_to_list(str):
# Split the comma separated string into a list # Split the comma separated string into a list
l = [i.strip() for i in str.split(',')] l = [i.strip() for i in str.split(",")]
# Remove empty strings # Remove empty strings
return list(filter(None, l)) 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): def process_event(github_token, github_repository, repo, branch, base):
# Fetch optional environment variables with default values # Fetch optional environment variables with default values
commit_message = os.getenv( commit_message = os.getenv(
'COMMIT_MESSAGE', "COMMIT_MESSAGE", "Auto-committed changes by create-pull-request action"
"Auto-committed changes by create-pull-request action") )
title = os.getenv( title = os.getenv(
'PULL_REQUEST_TITLE', "PULL_REQUEST_TITLE", "Auto-generated by create-pull-request action"
"Auto-generated by create-pull-request action") )
body = os.getenv( body = os.getenv(
'PULL_REQUEST_BODY', "Auto-generated pull request by " "PULL_REQUEST_BODY",
"[create-pull-request](https://github.com/peter-evans/create-pull-request) GitHub Action") "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 # Fetch optional environment variables with no default values
pull_request_labels = os.environ.get('PULL_REQUEST_LABELS') pull_request_labels = os.environ.get("PULL_REQUEST_LABELS")
pull_request_assignees = os.environ.get('PULL_REQUEST_ASSIGNEES') pull_request_assignees = os.environ.get("PULL_REQUEST_ASSIGNEES")
pull_request_milestone = os.environ.get('PULL_REQUEST_MILESTONE') pull_request_milestone = os.environ.get("PULL_REQUEST_MILESTONE")
pull_request_reviewers = os.environ.get('PULL_REQUEST_REVIEWERS') pull_request_reviewers = os.environ.get("PULL_REQUEST_REVIEWERS")
pull_request_team_reviewers = os.environ.get('PULL_REQUEST_TEAM_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 # Push the local changes to the remote branch
print("Pushing changes to 'origin/%s'" % branch) print("Pushing changes to 'origin/%s'" % branch)
@ -111,34 +148,30 @@ def process_event(github_token, github_repository, repo, branch, base):
github_repo = Github(github_token).get_repo(github_repository) github_repo = Github(github_token).get_repo(github_repository)
try: try:
pull_request = github_repo.create_pull( pull_request = github_repo.create_pull(
title=title, title=title, body=body, base=base, head=branch
body=body, )
base=base, print(
head=branch) "Created pull request #%d (%s => %s)" % (pull_request.number, branch, base)
print("Created pull request #%d (%s => %s)" % )
(pull_request.number, branch, base))
except GithubException as e: except GithubException as e:
if e.status == 422: if e.status == 422:
# Format the branch name # Format the branch name
head_branch = "%s:%s" % (github_repository.split("/")[0], branch) head_branch = "%s:%s" % (github_repository.split("/")[0], branch)
# Get the pull request # Get the pull request
pull_request = github_repo.get_pulls( pull_request = github_repo.get_pulls(
state='open', state="open", base=base, head=head_branch
base=base, )[0]
head=head_branch)[0] print(
print("Updated pull request #%d (%s => %s)" % "Updated pull request #%d (%s => %s)"
(pull_request.number, branch, base)) % (pull_request.number, branch, base)
)
else: else:
print(str(e)) print(str(e))
sys.exit(1) sys.exit(1)
# Set the output variables # Set the output variables
os.system( os.system("echo ::set-env name=PULL_REQUEST_NUMBER::%d" % pull_request.number)
'echo ::set-env name=PULL_REQUEST_NUMBER::%d' % os.system("echo ::set-output name=pr_number::%d" % pull_request.number)
pull_request.number)
os.system(
'echo ::set-output name=pr_number::%d' %
pull_request.number)
# Set labels, assignees and milestone # Set labels, assignees and milestone
if pull_request_labels is not None: if pull_request_labels is not None:
@ -146,7 +179,9 @@ def process_event(github_token, github_repository, repo, branch, base):
pull_request.as_issue().edit(labels=cs_string_to_list(pull_request_labels)) pull_request.as_issue().edit(labels=cs_string_to_list(pull_request_labels))
if pull_request_assignees is not None: if pull_request_assignees is not None:
print("Applying assignees '%s'" % pull_request_assignees) print("Applying assignees '%s'" % pull_request_assignees)
pull_request.as_issue().edit(assignees=cs_string_to_list(pull_request_assignees)) pull_request.as_issue().edit(
assignees=cs_string_to_list(pull_request_assignees)
)
if pull_request_milestone is not None: if pull_request_milestone is not None:
print("Applying milestone '%s'" % pull_request_milestone) print("Applying milestone '%s'" % pull_request_milestone)
milestone = github_repo.get_milestone(int(pull_request_milestone)) milestone = github_repo.get_milestone(int(pull_request_milestone))
@ -157,7 +192,8 @@ def process_event(github_token, github_repository, repo, branch, base):
print("Requesting reviewers '%s'" % pull_request_reviewers) print("Requesting reviewers '%s'" % pull_request_reviewers)
try: try:
pull_request.create_review_request( pull_request.create_review_request(
reviewers=cs_string_to_list(pull_request_reviewers)) reviewers=cs_string_to_list(pull_request_reviewers)
)
except GithubException as e: except GithubException as e:
# Likely caused by "Review cannot be requested from pull request # Likely caused by "Review cannot be requested from pull request
# author." # author."
@ -168,61 +204,78 @@ def process_event(github_token, github_repository, repo, branch, base):
if pull_request_team_reviewers is not None: if pull_request_team_reviewers is not None:
print("Requesting team reviewers '%s'" % pull_request_team_reviewers) print("Requesting team reviewers '%s'" % pull_request_team_reviewers)
pull_request.create_review_request( pull_request.create_review_request(
team_reviewers=cs_string_to_list(pull_request_team_reviewers)) 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 # Fetch environment variables
github_token = os.environ['GITHUB_TOKEN'] github_token = os.environ["GITHUB_TOKEN"]
github_repository = os.environ['GITHUB_REPOSITORY'] github_repository = os.environ["GITHUB_REPOSITORY"]
github_ref = os.environ['GITHUB_REF'] github_ref = os.environ["GITHUB_REF"]
event_name = os.environ['GITHUB_EVENT_NAME'] event_name = os.environ["GITHUB_EVENT_NAME"]
# Get the JSON event data # Get the JSON event data
event_data = get_github_event(os.environ['GITHUB_EVENT_PATH']) event_data = get_github_event(os.environ["GITHUB_EVENT_PATH"])
# Set the repo to the working directory # Set the repo to the working directory
repo = Repo(os.getcwd()) repo = Repo(os.getcwd())
# Get the default for author email and name # Get the default for author email and name
author_email, author_name = get_author_default(event_name, event_data) author_email, author_name = get_author_default(event_name, event_data)
# Set commit author overrides # Set commit author overrides
author_email = os.getenv('COMMIT_AUTHOR_EMAIL', author_email) author_email = os.getenv("COMMIT_AUTHOR_EMAIL", author_email)
author_name = os.getenv('COMMIT_AUTHOR_NAME', author_name) author_name = os.getenv("COMMIT_AUTHOR_NAME", author_name)
# Set git configuration # Set git configuration
set_git_config(repo.git, author_email, author_name) set_git_config(repo.git, author_email, author_name)
# Update URL for the 'origin' remote # Update URL for the 'origin' remote
set_git_remote_url(repo.git, github_token, github_repository) set_git_remote_url(repo.git, github_token, github_repository)
# Fetch/Set the branch name # Fetch/Set the branch name
branch_prefix = os.getenv( branch_prefix = os.getenv("PULL_REQUEST_BRANCH", "create-pull-request/patch")
'PULL_REQUEST_BRANCH',
'create-pull-request/patch')
# Fetch an optional base branch override # Fetch an optional base branch override
base_override = os.environ.get('PULL_REQUEST_BASE') base_override = os.environ.get("PULL_REQUEST_BASE")
# Set the base branch # Set the base branch
if base_override is not None: if base_override is not None:
base = base_override base = base_override
print("Overriding the base with branch '%s'" % base) print("Overriding the base with branch '%s'" % base)
checkout_branch(repo.git, True, base) checkout_branch(repo.git, True, base)
elif github_ref.startswith('refs/pull/'): elif github_ref.startswith("refs/pull/"):
# Check the PR is not raised from a fork of the repository # Check the PR is not raised from a fork of the repository
head_repo = "{pull_request[head][repo][full_name]}".format(**event_data) head_repo = "{pull_request[head][repo][full_name]}".format(**event_data)
if head_repo != github_repository: if head_repo != github_repository:
print("::warning::Pull request was raised from a fork of the repository. " + print(
"Limitations on forked repositories have been imposed by GitHub Actions. " + "::warning::Pull request was raised from a fork of the repository. "
"Unable to continue. Exiting.") + "Limitations on forked repositories have been imposed by GitHub Actions. "
+ "Unable to continue. Exiting."
)
sys.exit() sys.exit()
# Switch to the merging branch instead of the merge commit # Switch to the merging branch instead of the merge commit
base = os.environ['GITHUB_HEAD_REF'] base = os.environ["GITHUB_HEAD_REF"]
print( print(
"Removing the merge commit by switching to the pull request head branch '%s'" % "Removing the merge commit by switching to the pull request head branch '%s'"
base) % base
)
checkout_branch(repo.git, True, base) checkout_branch(repo.git, True, base)
elif github_ref.startswith('refs/heads/'): elif github_ref.startswith("refs/heads/"):
base = github_ref[11:] base = github_ref[11:]
print("Currently checked out base assumed to be branch '%s'" % base) print("Currently checked out base assumed to be branch '%s'" % base)
else: else:
print(f"::warning::Currently checked out ref '{github_ref}' is not a valid base for a pull request. " + print(
"Unable to continue. Exiting.") f"::warning::Currently checked out ref '{github_ref}' is not a valid base for a pull request. "
+ "Unable to continue. Exiting."
)
sys.exit() sys.exit()
# Skip if the current branch is a PR branch created by this action. # Skip if the current branch is a PR branch created by this action.
@ -233,7 +286,7 @@ if base.startswith(branch_prefix):
sys.exit() sys.exit()
# Fetch an optional environment variable to determine the branch suffix # Fetch an optional environment variable to determine the branch suffix
branch_suffix = os.getenv('BRANCH_SUFFIX', 'short-commit-hash') branch_suffix = os.getenv("BRANCH_SUFFIX", "short-commit-hash")
if branch_suffix == "short-commit-hash": if branch_suffix == "short-commit-hash":
# Suffix with the short SHA1 hash # Suffix with the short SHA1 hash
branch = "%s-%s" % (branch_prefix, get_head_short_sha1(repo)) branch = "%s-%s" % (branch_prefix, get_head_short_sha1(repo))
@ -247,9 +300,7 @@ elif branch_suffix == "none":
# Fixed branch name # Fixed branch name
branch = branch_prefix branch = branch_prefix
else: else:
print( print("Branch suffix '%s' is not a valid value." % branch_suffix)
"Branch suffix '%s' is not a valid value." %
branch_suffix)
sys.exit(1) sys.exit(1)
# Output head branch # Output head branch
@ -259,19 +310,22 @@ print("Pull request branch to create/update set to '%s'" % branch)
remote_exists = remote_branch_exists(repo, branch) remote_exists = remote_branch_exists(repo, branch)
if remote_exists: if remote_exists:
print( print(
"Pull request branch '%s' already exists as remote branch 'origin/%s'" % "Pull request branch '%s' already exists as remote branch 'origin/%s'"
(branch, branch)) % (branch, branch)
if branch_suffix == 'short-commit-hash': )
if branch_suffix == "short-commit-hash":
# A remote branch already exists for the HEAD commit # A remote branch already exists for the HEAD commit
print( print(
"Pull request branch '%s' already exists for this commit. Skipping." % "Pull request branch '%s' already exists for this commit. Skipping."
branch) % branch
)
sys.exit() sys.exit()
elif branch_suffix in ['timestamp', 'random']: elif branch_suffix in ["timestamp", "random"]:
# Generated branch name collision with an existing branch # Generated branch name collision with an existing branch
print( print(
"Pull request branch '%s' collided with a branch of the same name. Please re-run." % "Pull request branch '%s' collided with a branch of the same name. Please re-run."
branch) % branch
)
sys.exit(1) sys.exit(1)
# Checkout branch # Checkout branch
@ -279,19 +333,18 @@ checkout_branch(repo.git, remote_exists, branch)
# Check if there are changes to pull request # Check if there are changes to pull request
if remote_exists: if remote_exists:
print("Checking for local working copy changes indicating a " + print(
"diff with existing pull request branch 'origin/%s'" % branch) "Checking for local working copy changes indicating a "
+ "diff with existing pull request branch 'origin/%s'" % branch
)
else: else:
print("Checking for local working copy changes indicating a " + print(
"diff with base 'origin/%s'" % base) "Checking for local working copy changes indicating a "
+ "diff with base 'origin/%s'" % base
)
if repo.is_dirty() or len(repo.untracked_files) > 0: if repo.is_dirty() or len(repo.untracked_files) > 0:
print("Modified or untracked files detected.") print("Modified or untracked files detected.")
process_event( process_event(github_token, github_repository, repo, branch, base)
github_token,
github_repository,
repo,
branch,
base)
else: else:
print("No modified or untracked files detected. Skipping.") print("No modified or untracked files detected. Skipping.")

View file

@ -32,6 +32,8 @@ async function run() {
reviewers: core.getInput("reviewers"), reviewers: core.getInput("reviewers"),
teamReviewers: core.getInput("team-reviewers"), teamReviewers: core.getInput("team-reviewers"),
milestone: core.getInput("milestone"), milestone: core.getInput("milestone"),
project: core.getInput("project"),
projectColumn: core.getInput("project-column"),
branch: core.getInput("branch"), branch: core.getInput("branch"),
base: core.getInput("base"), base: core.getInput("base"),
branchSuffix: core.getInput("branch-suffix"), branchSuffix: core.getInput("branch-suffix"),
@ -51,6 +53,8 @@ async function run() {
if (inputs.reviewers) process.env.PULL_REQUEST_REVIEWERS = inputs.reviewers; if (inputs.reviewers) process.env.PULL_REQUEST_REVIEWERS = inputs.reviewers;
if (inputs.teamReviewers) process.env.PULL_REQUEST_TEAM_REVIEWERS = inputs.teamReviewers; if (inputs.teamReviewers) process.env.PULL_REQUEST_TEAM_REVIEWERS = inputs.teamReviewers;
if (inputs.milestone) process.env.PULL_REQUEST_MILESTONE = inputs.milestone; 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.branch) process.env.PULL_REQUEST_BRANCH = inputs.branch;
if (inputs.base) process.env.PULL_REQUEST_BASE = inputs.base; if (inputs.base) process.env.PULL_REQUEST_BASE = inputs.base;
if (inputs.branchSuffix) process.env.BRANCH_SUFFIX = inputs.branchSuffix; if (inputs.branchSuffix) process.env.BRANCH_SUFFIX = inputs.branchSuffix;

View file

@ -1,10 +1,10 @@
{ {
"name": "create-pull-request", "name": "create-pull-request",
"version": "1.7.4", "version": "1.8.0",
"description": "Creates a pull request for changes to your repository in the actions workspace", "description": "Creates a pull request for changes to your repository in the actions workspace",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"build": "ncc build index.js -o dist" "package": "ncc build index.js -o dist"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View file

@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
''' Create Pull Request ''' """ Create Pull Request """
import json import json
import os import os
import random import random
@ -13,18 +13,18 @@ from github import Github, GithubException
def get_github_event(github_event_path): def get_github_event(github_event_path):
with open(github_event_path) as f: with open(github_event_path) as f:
github_event = json.load(f) github_event = json.load(f)
if bool(os.environ.get('DEBUG_EVENT')): if bool(os.environ.get("DEBUG_EVENT")):
print(os.environ['GITHUB_EVENT_NAME']) print(os.environ["GITHUB_EVENT_NAME"])
print(json.dumps(github_event, sort_keys=True, indent=2)) print(json.dumps(github_event, sort_keys=True, indent=2))
return github_event return github_event
def get_head_short_sha1(repo): def get_head_short_sha1(repo):
return repo.git.rev_parse('--short', 'HEAD') return repo.git.rev_parse("--short", "HEAD")
def get_random_suffix(size=7, chars=string.ascii_lowercase + string.digits): def get_random_suffix(size=7, chars=string.ascii_lowercase + string.digits):
return ''.join(random.choice(chars) for _ in range(size)) return "".join(random.choice(chars) for _ in range(size))
def remote_branch_exists(repo, branch): def remote_branch_exists(repo, branch):
@ -39,68 +39,105 @@ def get_author_default(event_name, event_data):
email = "{head_commit[author][email]}".format(**event_data) email = "{head_commit[author][email]}".format(**event_data)
name = "{head_commit[author][name]}".format(**event_data) name = "{head_commit[author][name]}".format(**event_data)
else: else:
email = os.environ['GITHUB_ACTOR'] + '@users.noreply.github.com' email = os.environ["GITHUB_ACTOR"] + "@users.noreply.github.com"
name = os.environ['GITHUB_ACTOR'] name = os.environ["GITHUB_ACTOR"]
return email, name return email, name
def set_git_config(git, email, name): def set_git_config(git, email, name):
print("Configuring git user as '%s <%s>'" % (name, email)) print("Configuring git user as '%s <%s>'" % (name, email))
git.config('--global', 'user.email', '"%s"' % email) git.config("--global", "user.email", '"%s"' % email)
git.config('--global', 'user.name', '"%s"' % name) git.config("--global", "user.name", '"%s"' % name)
def set_git_remote_url(git, token, github_repository): def set_git_remote_url(git, token, github_repository):
git.remote( git.remote(
'set-url', 'origin', "https://x-access-token:%s@github.com/%s" % "set-url",
(token, github_repository)) "origin",
"https://x-access-token:%s@github.com/%s" % (token, github_repository),
)
def checkout_branch(git, remote_exists, branch): def checkout_branch(git, remote_exists, branch):
if remote_exists: if remote_exists:
print("Checking out branch '%s'" % branch) print("Checking out branch '%s'" % branch)
git.stash('--include-untracked') git.stash("--include-untracked")
git.checkout(branch) git.checkout(branch)
try: try:
git.stash('pop') git.stash("pop")
except BaseException: except BaseException:
git.checkout('--theirs', '.') git.checkout("--theirs", ".")
git.reset() git.reset()
else: else:
print("Creating new branch '%s'" % branch) print("Creating new branch '%s'" % branch)
git.checkout('HEAD', b=branch) git.checkout("HEAD", b=branch)
def push_changes(git, branch, commit_message): def push_changes(git, branch, commit_message):
git.add('-A') git.add("-A")
git.commit(m=commit_message) git.commit(m=commit_message)
return git.push('-f', '--set-upstream', 'origin', branch) return git.push("-f", "--set-upstream", "origin", branch)
def cs_string_to_list(str): def cs_string_to_list(str):
# Split the comma separated string into a list # Split the comma separated string into a list
l = [i.strip() for i in str.split(',')] l = [i.strip() for i in str.split(",")]
# Remove empty strings # Remove empty strings
return list(filter(None, l)) 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): def process_event(github_token, github_repository, repo, branch, base):
# Fetch optional environment variables with default values # Fetch optional environment variables with default values
commit_message = os.getenv( commit_message = os.getenv(
'COMMIT_MESSAGE', "COMMIT_MESSAGE", "Auto-committed changes by create-pull-request action"
"Auto-committed changes by create-pull-request action") )
title = os.getenv( title = os.getenv(
'PULL_REQUEST_TITLE', "PULL_REQUEST_TITLE", "Auto-generated by create-pull-request action"
"Auto-generated by create-pull-request action") )
body = os.getenv( body = os.getenv(
'PULL_REQUEST_BODY', "Auto-generated pull request by " "PULL_REQUEST_BODY",
"[create-pull-request](https://github.com/peter-evans/create-pull-request) GitHub Action") "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 # Fetch optional environment variables with no default values
pull_request_labels = os.environ.get('PULL_REQUEST_LABELS') pull_request_labels = os.environ.get("PULL_REQUEST_LABELS")
pull_request_assignees = os.environ.get('PULL_REQUEST_ASSIGNEES') pull_request_assignees = os.environ.get("PULL_REQUEST_ASSIGNEES")
pull_request_milestone = os.environ.get('PULL_REQUEST_MILESTONE') pull_request_milestone = os.environ.get("PULL_REQUEST_MILESTONE")
pull_request_reviewers = os.environ.get('PULL_REQUEST_REVIEWERS') pull_request_reviewers = os.environ.get("PULL_REQUEST_REVIEWERS")
pull_request_team_reviewers = os.environ.get('PULL_REQUEST_TEAM_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 # Push the local changes to the remote branch
print("Pushing changes to 'origin/%s'" % branch) print("Pushing changes to 'origin/%s'" % branch)
@ -111,34 +148,30 @@ def process_event(github_token, github_repository, repo, branch, base):
github_repo = Github(github_token).get_repo(github_repository) github_repo = Github(github_token).get_repo(github_repository)
try: try:
pull_request = github_repo.create_pull( pull_request = github_repo.create_pull(
title=title, title=title, body=body, base=base, head=branch
body=body, )
base=base, print(
head=branch) "Created pull request #%d (%s => %s)" % (pull_request.number, branch, base)
print("Created pull request #%d (%s => %s)" % )
(pull_request.number, branch, base))
except GithubException as e: except GithubException as e:
if e.status == 422: if e.status == 422:
# Format the branch name # Format the branch name
head_branch = "%s:%s" % (github_repository.split("/")[0], branch) head_branch = "%s:%s" % (github_repository.split("/")[0], branch)
# Get the pull request # Get the pull request
pull_request = github_repo.get_pulls( pull_request = github_repo.get_pulls(
state='open', state="open", base=base, head=head_branch
base=base, )[0]
head=head_branch)[0] print(
print("Updated pull request #%d (%s => %s)" % "Updated pull request #%d (%s => %s)"
(pull_request.number, branch, base)) % (pull_request.number, branch, base)
)
else: else:
print(str(e)) print(str(e))
sys.exit(1) sys.exit(1)
# Set the output variables # Set the output variables
os.system( os.system("echo ::set-env name=PULL_REQUEST_NUMBER::%d" % pull_request.number)
'echo ::set-env name=PULL_REQUEST_NUMBER::%d' % os.system("echo ::set-output name=pr_number::%d" % pull_request.number)
pull_request.number)
os.system(
'echo ::set-output name=pr_number::%d' %
pull_request.number)
# Set labels, assignees and milestone # Set labels, assignees and milestone
if pull_request_labels is not None: if pull_request_labels is not None:
@ -146,7 +179,9 @@ def process_event(github_token, github_repository, repo, branch, base):
pull_request.as_issue().edit(labels=cs_string_to_list(pull_request_labels)) pull_request.as_issue().edit(labels=cs_string_to_list(pull_request_labels))
if pull_request_assignees is not None: if pull_request_assignees is not None:
print("Applying assignees '%s'" % pull_request_assignees) print("Applying assignees '%s'" % pull_request_assignees)
pull_request.as_issue().edit(assignees=cs_string_to_list(pull_request_assignees)) pull_request.as_issue().edit(
assignees=cs_string_to_list(pull_request_assignees)
)
if pull_request_milestone is not None: if pull_request_milestone is not None:
print("Applying milestone '%s'" % pull_request_milestone) print("Applying milestone '%s'" % pull_request_milestone)
milestone = github_repo.get_milestone(int(pull_request_milestone)) milestone = github_repo.get_milestone(int(pull_request_milestone))
@ -157,7 +192,8 @@ def process_event(github_token, github_repository, repo, branch, base):
print("Requesting reviewers '%s'" % pull_request_reviewers) print("Requesting reviewers '%s'" % pull_request_reviewers)
try: try:
pull_request.create_review_request( pull_request.create_review_request(
reviewers=cs_string_to_list(pull_request_reviewers)) reviewers=cs_string_to_list(pull_request_reviewers)
)
except GithubException as e: except GithubException as e:
# Likely caused by "Review cannot be requested from pull request # Likely caused by "Review cannot be requested from pull request
# author." # author."
@ -168,61 +204,78 @@ def process_event(github_token, github_repository, repo, branch, base):
if pull_request_team_reviewers is not None: if pull_request_team_reviewers is not None:
print("Requesting team reviewers '%s'" % pull_request_team_reviewers) print("Requesting team reviewers '%s'" % pull_request_team_reviewers)
pull_request.create_review_request( pull_request.create_review_request(
team_reviewers=cs_string_to_list(pull_request_team_reviewers)) 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 # Fetch environment variables
github_token = os.environ['GITHUB_TOKEN'] github_token = os.environ["GITHUB_TOKEN"]
github_repository = os.environ['GITHUB_REPOSITORY'] github_repository = os.environ["GITHUB_REPOSITORY"]
github_ref = os.environ['GITHUB_REF'] github_ref = os.environ["GITHUB_REF"]
event_name = os.environ['GITHUB_EVENT_NAME'] event_name = os.environ["GITHUB_EVENT_NAME"]
# Get the JSON event data # Get the JSON event data
event_data = get_github_event(os.environ['GITHUB_EVENT_PATH']) event_data = get_github_event(os.environ["GITHUB_EVENT_PATH"])
# Set the repo to the working directory # Set the repo to the working directory
repo = Repo(os.getcwd()) repo = Repo(os.getcwd())
# Get the default for author email and name # Get the default for author email and name
author_email, author_name = get_author_default(event_name, event_data) author_email, author_name = get_author_default(event_name, event_data)
# Set commit author overrides # Set commit author overrides
author_email = os.getenv('COMMIT_AUTHOR_EMAIL', author_email) author_email = os.getenv("COMMIT_AUTHOR_EMAIL", author_email)
author_name = os.getenv('COMMIT_AUTHOR_NAME', author_name) author_name = os.getenv("COMMIT_AUTHOR_NAME", author_name)
# Set git configuration # Set git configuration
set_git_config(repo.git, author_email, author_name) set_git_config(repo.git, author_email, author_name)
# Update URL for the 'origin' remote # Update URL for the 'origin' remote
set_git_remote_url(repo.git, github_token, github_repository) set_git_remote_url(repo.git, github_token, github_repository)
# Fetch/Set the branch name # Fetch/Set the branch name
branch_prefix = os.getenv( branch_prefix = os.getenv("PULL_REQUEST_BRANCH", "create-pull-request/patch")
'PULL_REQUEST_BRANCH',
'create-pull-request/patch')
# Fetch an optional base branch override # Fetch an optional base branch override
base_override = os.environ.get('PULL_REQUEST_BASE') base_override = os.environ.get("PULL_REQUEST_BASE")
# Set the base branch # Set the base branch
if base_override is not None: if base_override is not None:
base = base_override base = base_override
print("Overriding the base with branch '%s'" % base) print("Overriding the base with branch '%s'" % base)
checkout_branch(repo.git, True, base) checkout_branch(repo.git, True, base)
elif github_ref.startswith('refs/pull/'): elif github_ref.startswith("refs/pull/"):
# Check the PR is not raised from a fork of the repository # Check the PR is not raised from a fork of the repository
head_repo = "{pull_request[head][repo][full_name]}".format(**event_data) head_repo = "{pull_request[head][repo][full_name]}".format(**event_data)
if head_repo != github_repository: if head_repo != github_repository:
print("::warning::Pull request was raised from a fork of the repository. " + print(
"Limitations on forked repositories have been imposed by GitHub Actions. " + "::warning::Pull request was raised from a fork of the repository. "
"Unable to continue. Exiting.") + "Limitations on forked repositories have been imposed by GitHub Actions. "
+ "Unable to continue. Exiting."
)
sys.exit() sys.exit()
# Switch to the merging branch instead of the merge commit # Switch to the merging branch instead of the merge commit
base = os.environ['GITHUB_HEAD_REF'] base = os.environ["GITHUB_HEAD_REF"]
print( print(
"Removing the merge commit by switching to the pull request head branch '%s'" % "Removing the merge commit by switching to the pull request head branch '%s'"
base) % base
)
checkout_branch(repo.git, True, base) checkout_branch(repo.git, True, base)
elif github_ref.startswith('refs/heads/'): elif github_ref.startswith("refs/heads/"):
base = github_ref[11:] base = github_ref[11:]
print("Currently checked out base assumed to be branch '%s'" % base) print("Currently checked out base assumed to be branch '%s'" % base)
else: else:
print(f"::warning::Currently checked out ref '{github_ref}' is not a valid base for a pull request. " + print(
"Unable to continue. Exiting.") f"::warning::Currently checked out ref '{github_ref}' is not a valid base for a pull request. "
+ "Unable to continue. Exiting."
)
sys.exit() sys.exit()
# Skip if the current branch is a PR branch created by this action. # Skip if the current branch is a PR branch created by this action.
@ -233,7 +286,7 @@ if base.startswith(branch_prefix):
sys.exit() sys.exit()
# Fetch an optional environment variable to determine the branch suffix # Fetch an optional environment variable to determine the branch suffix
branch_suffix = os.getenv('BRANCH_SUFFIX', 'short-commit-hash') branch_suffix = os.getenv("BRANCH_SUFFIX", "short-commit-hash")
if branch_suffix == "short-commit-hash": if branch_suffix == "short-commit-hash":
# Suffix with the short SHA1 hash # Suffix with the short SHA1 hash
branch = "%s-%s" % (branch_prefix, get_head_short_sha1(repo)) branch = "%s-%s" % (branch_prefix, get_head_short_sha1(repo))
@ -247,9 +300,7 @@ elif branch_suffix == "none":
# Fixed branch name # Fixed branch name
branch = branch_prefix branch = branch_prefix
else: else:
print( print("Branch suffix '%s' is not a valid value." % branch_suffix)
"Branch suffix '%s' is not a valid value." %
branch_suffix)
sys.exit(1) sys.exit(1)
# Output head branch # Output head branch
@ -259,19 +310,22 @@ print("Pull request branch to create/update set to '%s'" % branch)
remote_exists = remote_branch_exists(repo, branch) remote_exists = remote_branch_exists(repo, branch)
if remote_exists: if remote_exists:
print( print(
"Pull request branch '%s' already exists as remote branch 'origin/%s'" % "Pull request branch '%s' already exists as remote branch 'origin/%s'"
(branch, branch)) % (branch, branch)
if branch_suffix == 'short-commit-hash': )
if branch_suffix == "short-commit-hash":
# A remote branch already exists for the HEAD commit # A remote branch already exists for the HEAD commit
print( print(
"Pull request branch '%s' already exists for this commit. Skipping." % "Pull request branch '%s' already exists for this commit. Skipping."
branch) % branch
)
sys.exit() sys.exit()
elif branch_suffix in ['timestamp', 'random']: elif branch_suffix in ["timestamp", "random"]:
# Generated branch name collision with an existing branch # Generated branch name collision with an existing branch
print( print(
"Pull request branch '%s' collided with a branch of the same name. Please re-run." % "Pull request branch '%s' collided with a branch of the same name. Please re-run."
branch) % branch
)
sys.exit(1) sys.exit(1)
# Checkout branch # Checkout branch
@ -279,19 +333,18 @@ checkout_branch(repo.git, remote_exists, branch)
# Check if there are changes to pull request # Check if there are changes to pull request
if remote_exists: if remote_exists:
print("Checking for local working copy changes indicating a " + print(
"diff with existing pull request branch 'origin/%s'" % branch) "Checking for local working copy changes indicating a "
+ "diff with existing pull request branch 'origin/%s'" % branch
)
else: else:
print("Checking for local working copy changes indicating a " + print(
"diff with base 'origin/%s'" % base) "Checking for local working copy changes indicating a "
+ "diff with base 'origin/%s'" % base
)
if repo.is_dirty() or len(repo.untracked_files) > 0: if repo.is_dirty() or len(repo.untracked_files) > 0:
print("Modified or untracked files detected.") print("Modified or untracked files detected.")
process_event( process_event(github_token, github_repository, repo, branch, base)
github_token,
github_repository,
repo,
branch,
base)
else: else:
print("No modified or untracked files detected. Skipping.") print("No modified or untracked files detected. Skipping.")