Merge pull request #90 from peter-evans/v2-beta

v2 beta
This commit is contained in:
Peter Evans 2020-01-03 14:18:38 +09:00 committed by GitHub
commit 938e6aea6f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 4420 additions and 1324 deletions

View file

@ -1,46 +0,0 @@
name: Create Pull Request All Platforms
on:
repository_dispatch:
types: [create-pull-request-multi]
jobs:
createPullRequest:
name: Testing on ${{ matrix.platform }}
strategy:
matrix:
platform: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v1
- 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
author-email: peter-evans@users.noreply.github.com
author-name: 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 }}"

View file

@ -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 <peter-evans@users.noreply.github.com>
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 }}"

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
__pycache__
node_modules
.DS_Store

117
README.md
View file

@ -9,26 +9,29 @@ 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&mdash;the branch checked out in the workflow.
## Documentation
- [Concepts and guidelines](docs/concepts-guidelines.md)
- [Examples](docs/examples.md)
- [Updating from v1](docs/updating.md)
## Usage
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
with:
token: ${{ secrets.GITHUB_TOKEN }}
```
Branch [v2-beta](https://github.com/peter-evans/create-pull-request/tree/v2-beta) is the beta testing release of a major version update.
Try it out using `peter-evans/create-pull-request@v2-beta`.
See [this documentation](https://github.com/peter-evans/create-pull-request/blob/v2-beta/updating.md) for details of breaking changes and new features.
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.
@ -39,13 +42,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, <GITHUB_ACTOR>, the GitHub user that initiated the event. |
| `author-email` | The email address of the commit author. | For `push` events, the HEAD commit author. Otherwise, <GITHUB_ACTOR>@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 <email@address.com>`. | 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 <email@address.com>`. | 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. | |
@ -53,9 +54,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**
@ -65,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@v1
uses: peter-evans/create-pull-request@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Check outputs
@ -74,49 +75,82 @@ 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. 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
with:
token: ${{ secrets.GITHUB_TOKEN }}
committer: Peter Evans <peter-evans@users.noreply.github.com>
```
### 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
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
```
## Reference Example
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
@ -125,17 +159,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
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 <peter-evans@users.noreply.github.com>
author: Peter Evans <peter-evans@users.noreply.github.com>
title: '[Example] Add report file'
body: |
New report
@ -150,7 +184,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 }}"

View file

@ -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:

68
assets/cpr-gitgraph.htm Normal file
View file

@ -0,0 +1,68 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>create-pull-request GitHub action</title>
</head>
<body>
<!-- partial:index.partial.html -->
<div id="graph-container"></div>
<!-- partial -->
<script src='https://cdn.jsdelivr.net/npm/@gitgraph/js'></script>
<script>
const graphContainer = document.getElementById("graph-container");
const customTemplate = GitgraphJS.templateExtend(GitgraphJS.TemplateName.Metro, {
commit: {
message: {
displayAuthor: false,
displayHash: false,
},
},
});
// Instantiate the graph.
const gitgraph = GitgraphJS.createGitgraph(graphContainer, {
template: customTemplate,
orientation: "vertical-reverse"
});
const master = gitgraph.branch("master");
master.commit("Last commit on base");
const localMaster = gitgraph.branch("<#1> master (local)");
localMaster.commit({
subject: "<uncommited changes>",
body: "Changes made to the local base during the workflow",
})
const remotePatch = gitgraph.branch("create-pull-request/patch");
remotePatch.merge({
branch: localMaster,
commitOptions: {
subject: "[create-pull-request] automated change",
body: "Changes pushed to create the remote branch",
},
});
master.commit("New commit on base");
const localMaster2 = gitgraph.branch("<#2> master (local)");
localMaster2.commit({
subject: "<uncommited changes>",
body: "Changes made to the updated local base during the workflow",
})
remotePatch.merge({
branch: localMaster2,
commitOptions: {
subject: "[create-pull-request] automated change",
body: "Changes force pushed to update the remote branch",
},
});
master.merge(remotePatch);
</script>
</body>
</html>

BIN
assets/cpr-gitgraph.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

1940
dist/index.js vendored

File diff suppressed because it is too large Load diff

31
dist/src/common.py vendored Normal file
View file

@ -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 <email@address.com>
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

View file

@ -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.")

145
dist/src/create_or_update_branch.py vendored Normal file
View file

@ -0,0 +1,145 @@
#!/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}

View file

@ -0,0 +1,126 @@
#!/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("::error::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("::error::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(f"Created pull request #{pull_request.number} ({branch} => {base})")
except GithubException as e:
if e.status == 422:
# 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(f"Updated pull request #{pull_request.number} ({branch} => {base})")
else:
print(str(e))
raise
# Set the output variables
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(f"Applying labels '{labels}'")
pull_request.as_issue().edit(labels=cs_string_to_list(labels))
if assignees is not None:
print(f"Applying assignees '{assignees}'")
pull_request.as_issue().edit(assignees=cs_string_to_list(assignees))
if milestone is not None:
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(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."
if e.status == 422:
print("Request reviewers failed - {}".format(e.data["message"]))
# Set pull request team reviewers
if team_reviewers is not None:
print(f"Requesting team reviewers '{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 - {}".format(
e.data["errors"][0]["message"]
)
)

204
dist/src/create_pull_request.py vendored Executable file
View file

@ -0,0 +1,204 @@
#!/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, GitCommandError
import json
import os
import sys
import time
# Default the committer and author to the GitHub Actions bot
DEFAULT_COMMITTER = "GitHub <noreply@github.com>"
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):
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:
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
# 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:
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)
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
github_token = os.environ["GITHUB_TOKEN"]
github_repository = os.environ["GITHUB_REPOSITORY"]
# Get environment variables with defaults
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")
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 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."
)
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"]
# 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()
# Fetch optional environment variables with default values
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(
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"),
)

View file

@ -1,2 +1,2 @@
GitPython==3.0.5
PyGithub==1.44.1
PyGithub==1.45

39
dist/src/test_common.py vendored Normal file
View file

@ -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 <abc@def.com>")
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"
)

757
dist/src/test_create_or_update_branch.py vendored Normal file
View file

@ -0,0 +1,757 @@
#!/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
# 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.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 = "[create-pull-request] automated change"
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(os.path.join(REPO_PATH, 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(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(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(os.path.join(REPO_PATH, 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.remotes.origin.fetch()
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
)

107
docs/concepts-guidelines.md Normal file
View file

@ -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`&mdash;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.
![Create Pull Request GitGraph](../assets/cpr-gitgraph.png)
## 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
```

View file

@ -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
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
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
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
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
@ -229,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.
@ -253,7 +251,9 @@ 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
with:
ref: ${{ github.head_ref }}
- name: autopep8
id: autopep8
uses: peter-evans/autopep8@v1.1.0
@ -261,10 +261,10 @@ 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@v1
uses: peter-evans/create-pull-request@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: autopep8 action fixes
@ -272,7 +272,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 +299,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 +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@v1
uses: peter-evans/create-pull-request@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
title: ${{ steps.vars.outputs.pr_title }}
@ -340,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@v1
uses: peter-evans/create-pull-request@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
title: ${{ env.PULL_REQUEST_TITLE }}

25
docs/updating.md Normal file
View file

@ -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 <email@address.com>`
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. 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.

View file

@ -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);
}

23
package-lock.json generated
View file

@ -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"
}

View file

@ -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": {

31
src/common.py Normal file
View file

@ -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 <email@address.com>
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

View file

@ -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.")

View file

@ -0,0 +1,145 @@
#!/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}

View file

@ -0,0 +1,126 @@
#!/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("::error::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("::error::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(f"Created pull request #{pull_request.number} ({branch} => {base})")
except GithubException as e:
if e.status == 422:
# 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(f"Updated pull request #{pull_request.number} ({branch} => {base})")
else:
print(str(e))
raise
# Set the output variables
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(f"Applying labels '{labels}'")
pull_request.as_issue().edit(labels=cs_string_to_list(labels))
if assignees is not None:
print(f"Applying assignees '{assignees}'")
pull_request.as_issue().edit(assignees=cs_string_to_list(assignees))
if milestone is not None:
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(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."
if e.status == 422:
print("Request reviewers failed - {}".format(e.data["message"]))
# Set pull request team reviewers
if team_reviewers is not None:
print(f"Requesting team reviewers '{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 - {}".format(
e.data["errors"][0]["message"]
)
)

204
src/create_pull_request.py Executable file
View file

@ -0,0 +1,204 @@
#!/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, GitCommandError
import json
import os
import sys
import time
# Default the committer and author to the GitHub Actions bot
DEFAULT_COMMITTER = "GitHub <noreply@github.com>"
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):
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:
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
# 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:
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)
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
github_token = os.environ["GITHUB_TOKEN"]
github_repository = os.environ["GITHUB_REPOSITORY"]
# Get environment variables with defaults
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")
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 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."
)
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"]
# 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()
# Fetch optional environment variables with default values
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(
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"),
)

View file

@ -1,2 +1,2 @@
GitPython==3.0.5
PyGithub==1.44.1
PyGithub==1.45

39
src/test_common.py Normal file
View file

@ -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 <abc@def.com>")
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"
)

View file

@ -0,0 +1,757 @@
#!/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
# 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.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 = "[create-pull-request] automated change"
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(os.path.join(REPO_PATH, 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(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(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(os.path.join(REPO_PATH, 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.remotes.origin.fetch()
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
)