24 Commits

Author SHA1 Message Date
vsoch
89160a5742 customize args for decorator
Signed-off-by: vsoch <vsoch@users.noreply.github.com>
2023-02-14 18:13:51 -07:00
vsoch
b57393d6ea add retry in case rate limit exceeded
Signed-off-by: vsoch <vsoch@users.noreply.github.com>
2023-02-11 19:00:26 -07:00
Vanessasaurus
f9e7f419d6 Merge pull request #94 from vsoch/check-for-branch
Check for PR branch
2023-01-11 12:39:17 -07:00
Vanessasaurus
dcba08c9c2 Check for PR branch
before using the API so we hopefully save a call.
2023-01-06 20:03:27 -07:00
Yoshihisa Mochihara
c8e84cea4c fix GITHUB_OUTPUT bug (#92)
* fix GITHUB_OUTPUT bug
* Update CHANGELOG.md
2022-11-23 14:02:13 -07:00
Vanessasaurus
50f22f6d14 Update/set output (#90)
* updating set-output to echo to $GITHUB_OUTPUT instead

Signed-off-by: vsoch <vsoch@users.noreply.github.com>
2022-10-25 21:00:21 -06:00
Yunqi(Alice)
d703f40f3a Update the list_pull_request 401 case (#89)
* Update the list_pull_request 401 case
Hi, I find this http request is also missing the 401 case.
* Update CHANGELOG.md
2022-09-28 10:03:05 -06:00
Vanessasaurus
415544869c Merge pull request #88 from ohenak/fix/ghe-get-401-error
[Chore] Provide token when getting 401 error
2022-09-22 03:59:00 -06:00
Kane Ho
667293445b [Chore] Provide token when getting 401 error
Issue #87

In GHE, token is necessary when querying the repo API,
When it fails, it returns 401 error. Will need to specify the token in
this case.
2022-09-21 23:44:03 -07:00
Felix Breidenstein
e64deeba68 Fix example in README (#85) 2022-03-18 08:44:13 -06:00
Vanessasaurus
58a078aed9 Update CHANGELOG.md 2022-02-03 06:00:46 -07:00
Sviatoslav Sydorenko
408336c607 Fix emitting LF-separated GHA environment vars (#84)
Fixes #83 (hopefully)
2022-02-03 05:59:59 -07:00
Vanessasaurus
d99aa3ed71 Adding quick option to be able to specify a GitHub enterprise url (#80)
* adding quick option to be able to specify a GitHub enterprise url
Signed-off-by: vsoch <vsoch@users.noreply.github.com>
* Use GITHUB_API_URL instead
Co-authored-by: Tobias <github@tobiasgabriel.de>

* Update README.md
* Update pull-request.py

Co-authored-by: vsoch <vsoch@users.noreply.github.com>
Co-authored-by: Tobias <github@tobiasgabriel.de>
2021-10-02 22:40:48 -06:00
Vanessasaurus
a48b660567 Merge pull request #81 from vsoch/add/open-source-halloween-2021
adding open source halloween!
2021-10-02 18:26:20 -06:00
vsoch
4b09070b0a adding open source halloween!
Signed-off-by: vsoch <vsoch@users.noreply.github.com>
2021-10-02 18:25:26 -06:00
Vanessasaurus
29dbfc0acd Merge pull request #75 from Kong/feat/discover-default-branch
Use repo's default branch if no branch specified
2021-06-21 10:10:40 -06:00
Danny Sauer
c0bf76830b Use repo's default branch if no branch specified 2021-06-21 11:04:52 -05:00
Yi Zhang
1cb1956a68 Support to open pull request to another repo (#70)
* support custom token and repo
* print post url if there's error
* update as comments
* Update README.md

Co-authored-by: Vanessasaurus <814322+vsoch@users.noreply.github.com>
2021-04-27 06:04:20 -06:00
vsoch
2ca266185d updating changelog preparing for release
Signed-off-by: vsoch <vsoch@users.noreply.github.com>
2021-04-18 19:54:26 -06:00
Max Anderson
e2a437cdbd Prevent unhandled exception when running via schedule (#69)
* Prevent unhandled exception when running via schedule
2021-04-18 19:53:12 -06:00
Vanessasaurus
2d5c80f215 dont use version for examples, stick to master (#67)
* dont use version for examples, stick to master
* add note about it being important to use stable release in README.md

Signed-off-by: vsoch <vsoch@users.noreply.github.com>
2021-03-27 19:08:35 -06:00
Vanessasaurus
7f0d383d15 adding back assignees variable (#65)
Signed-off-by: vsoch <vsoch@users.noreply.github.com>
2021-02-15 10:51:43 -07:00
Vanessasaurus
6d137cdd42 adding changes to update a PR (#60)
* adding changes to update a pr
including a second envar to set a state, along with refactor
of main body to be multiple functions, and adding docstrings
* fixing missing variable
* Try removing maintainer can modify
* Update pull-request.py

Signed-off-by: vsoch <vsoch@users.noreply.github.com>
2021-02-12 17:42:23 -07:00
Vanessasaurus
ebce8e4918 making abort_if_fail more verbose (#57)
Signed-off-by: vsoch <vsochat@stanford.edu>
2020-12-18 04:05:17 -07:00
11 changed files with 481 additions and 310 deletions

View File

@@ -14,6 +14,11 @@ represented by the pull requests that fixed them. Critical items to know are:
Versions correspond with GitHub releases that can be referenced with @ using actions. Versions correspond with GitHub releases that can be referenced with @ using actions.
## [master](https://github.com/vsoch/pull-request-action/tree/master) (master) ## [master](https://github.com/vsoch/pull-request-action/tree/master) (master)
- bugfix of missing output values (1.0.23)
- bugfix of token handling if 401 error received (missing 401 case) (1.0.21)
- bugfix of writing to environment file (missing newline) (1.0.19)
- bugfix of missing from branch with scheduled run (1.0.16)
- forgot to add assignees (1.0.15)
- output and environment variables for PR number and return codes (1.0.5) - output and environment variables for PR number and return codes (1.0.5)
- added support for reviewer (individual and team) assignments (1.0.4) - added support for reviewer (individual and team) assignments (1.0.4)
- added support for maintainer can modify and assignees (1.0.3) - added support for maintainer can modify and assignees (1.0.3)

View File

@@ -22,13 +22,16 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: pull-request-action - name: pull-request-action
uses: vsoch/pull-request-action@1.0.6 uses: vsoch/pull-request-action@master
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRANCH_PREFIX: "update/" BRANCH_PREFIX: "update/"
PULL_REQUEST_BRANCH: "master" PULL_REQUEST_BRANCH: "master"
``` ```
**Important**: Make sure to use a stable [release](https://github.com/vsoch/pull-request-action/releases) instead of a branch for your workflow.
## Environment Variable Inputs ## Environment Variable Inputs
Unlike standard actions, this action just uses variables from the environment. Unlike standard actions, this action just uses variables from the environment.
@@ -36,6 +39,8 @@ Unlike standard actions, this action just uses variables from the environment.
| Name | Description | Required | Default | | Name | Description | Required | Default |
|------|-------------|----------|---------| |------|-------------|----------|---------|
| BRANCH_PREFIX | the prefix to filter to. If the branch doesn't start with the prefix, it will be ignored | false | "" | | BRANCH_PREFIX | the prefix to filter to. If the branch doesn't start with the prefix, it will be ignored | false | "" |
| PULL_REQUEST_REPOSITORY | Choose another repository instead of default GITHUB_REPOSITORY for the PR | false | |
| PULL_REQUEST_TOKEN | [Personal Access Token(PAT)](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) only if you define a different repository with PULL_REQUEST_REPOSITORY | false | |
| PULL_REQUEST_BRANCH | open pull request against this branch | false | master | | PULL_REQUEST_BRANCH | open pull request against this branch | false | master |
| PULL_REQUEST_FROM_BRANCH | if a branch isn't found in your GitHub payload, use this branch | false | | | PULL_REQUEST_FROM_BRANCH | if a branch isn't found in your GitHub payload, use this branch | false | |
| PULL_REQUEST_BODY | the body for the pull request | false | | | PULL_REQUEST_BODY | the body for the pull request | false | |
@@ -45,8 +50,10 @@ Unlike standard actions, this action just uses variables from the environment.
| PULL_REQUEST_ASSIGNEES | A list (string with spaces) of users to assign | false | unset | | PULL_REQUEST_ASSIGNEES | A list (string with spaces) of users to assign | false | unset |
| PULL_REQUEST_REVIEWERS | A list (string with spaces) of users to assign review | false | unset | | PULL_REQUEST_REVIEWERS | A list (string with spaces) of users to assign review | false | unset |
| PULL_REQUEST_TEAM_REVIEWERS | A list (string with spaces) of teams to assign review | false | unset | | PULL_REQUEST_TEAM_REVIEWERS | A list (string with spaces) of teams to assign review | false | unset |
| PASS_ON_ERROR | Instead of failing on an error response, pass | unset | | PASS_ON_ERROR | Instead of failing on an error response, pass | false | unset |
| PASS_IF_EXISTS | Instead of failing if the pull request already exists, pass | unset | | PASS_IF_EXISTS | Instead of failing if the pull request already exists, pass | false | unset |
| PULL_REQUEST_UPDATE | If the pull request already exists, update it | false | unset |
| PULL_REQUEST_STATE | If `PULL_REQUEST_UPDATE` is true, update to this state (open, closed) | false |open |
For `PULL_REQUEST_DRAFT`, `PASS_ON_ERROR`, `PASS_IF_EXISTS`, and `MAINTAINER_CANT_MODIFY`, these are For `PULL_REQUEST_DRAFT`, `PASS_ON_ERROR`, `PASS_IF_EXISTS`, and `MAINTAINER_CANT_MODIFY`, these are
treated as environment booleans. If they are defined in the environment, they trigger the treated as environment booleans. If they are defined in the environment, they trigger the
@@ -56,6 +63,7 @@ treated as environment booleans. If they are defined in the environment, they tr
- Define `PULL_REQUEST_DRAFT` if you want the PR to be a draft. - Define `PULL_REQUEST_DRAFT` if you want the PR to be a draft.
- Define `PASS_ON_ERROR` if you want the PR to not exit given any non 200/201 response. - Define `PASS_ON_ERROR` if you want the PR to not exit given any non 200/201 response.
- Define `PASS_IF_EXISTS` if you want the PR to not exit given the pull request is already open. - Define `PASS_IF_EXISTS` if you want the PR to not exit given the pull request is already open.
- Define `PULL_REQUEST_UPDATE` if you want the pull request to be updated if it already exits.
For `PULL_REQUEST_ASSIGNEES`, `PULL_REQUEST_REVIEWERS`, and `PULL_REQUEST_TEAM_REVIEWERS` For `PULL_REQUEST_ASSIGNEES`, `PULL_REQUEST_REVIEWERS`, and `PULL_REQUEST_TEAM_REVIEWERS`
you can provide a string of one or more GitHub usernames (or team names) to you can provide a string of one or more GitHub usernames (or team names) to
@@ -65,6 +73,8 @@ an issue or PR, they are ignored otherwise.
The `GITHUB_TOKEN` secret is required to interact and authenticate with the GitHub API to open The `GITHUB_TOKEN` secret is required to interact and authenticate with the GitHub API to open
the pull request. The example is [deployed here](https://github.com/vsoch/pull-request-action-example) with an example opened (and merged) [pull request here](https://github.com/vsoch/pull-request-action-example/pull/1) if needed. the pull request. The example is [deployed here](https://github.com/vsoch/pull-request-action-example) with an example opened (and merged) [pull request here](https://github.com/vsoch/pull-request-action-example/pull/1) if needed.
If you want to create a pull request to another repository, for example, a pull request to the upstream repository, you need to define PULL_REQUEST_REPOSITORY and PULL_REQUEST_TOKEN. The PULL_REQUEST_TOKEN is one [Personal Access Token(PAT)](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token), which can be save in the [encrypted secrets](https://docs.github.com/en/actions/reference/encrypted-secrets#creating-encrypted-secrets-for-a-repository)
## Outputs ## Outputs
The action sets a few useful output and environment variables. An output can The action sets a few useful output and environment variables. An output can
@@ -90,7 +100,9 @@ The screenshot below shows the example in action to interact with outputs in sev
## Examples ## Examples
Example workflows are provided in [examples](examples), and please contribute any Example workflows are provided in [examples](examples), and please contribute any
examples that you might have to help other users! We will walk through a basic examples that you might have to help other users! You can get the same commit hashes
and commented tags if you use the [action-updater](https://github.com/vsoch/action-updater)
also maintained by @vsoch. We will walk through a basic
example here for a niche case. Let's say that we are opening a pull request on the release event. This would mean example here for a niche case. Let's say that we are opening a pull request on the release event. This would mean
that the payload's branch variable would be null. We would need to define `PULL_REQUEST_FROM`. How would that the payload's branch variable would be null. We would need to define `PULL_REQUEST_FROM`. How would
we do that? We can [set environment variables](https://github.com/actions/toolkit/blob/main/docs/commands.md#environment-files) for next steps. Here is an example: we do that? We can [set environment variables](https://github.com/actions/toolkit/blob/main/docs/commands.md#environment-files) for next steps. Here is an example:
@@ -109,9 +121,9 @@ jobs:
run: | run: |
# do custom parsing of your code / date to derive a branch from # do custom parsing of your code / date to derive a branch from
PR_BRANCH_FROM=release-v$(cat VERSION) PR_BRANCH_FROM=release-v$(cat VERSION)
export "PULL_REQUEST_FROM_BRANCH=${PR_BRANCH_FROM}" >> $GITHUB_ENV echo "PULL_REQUEST_FROM_BRANCH=${PR_BRANCH_FROM}" >> $GITHUB_ENV
- name: pull-request-action - name: pull-request-action
uses: vsoch/pull-request-action@1.0.6 uses: vsoch/pull-request-action@master
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PULL_REQUEST_BRANCH: "master" PULL_REQUEST_BRANCH: "master"

View File

@@ -9,9 +9,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: pull-request-action - name: pull-request-action
uses: vsoch/pull-request-action@1.0.3 uses: vsoch/pull-request-action@d703f40f3af5ae294f9816395ddf2e3d2d3feafa # 1.0.21
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRANCH_PREFIX: "update/" BRANCH_PREFIX: update/
PULL_REQUEST_BRANCH: "master" PULL_REQUEST_BRANCH: master
PULL_REQUEST_ASSIGNEES: vsoch PULL_REQUEST_ASSIGNEES: vsoch

View File

@@ -1,16 +1,15 @@
name: derive-branch-from-environment name: derive-branch-from-environment
on: on:
schedule: schedule: -
# Weekly cron: 0 0 * * 0
- cron: 0 0 * * 0
jobs: jobs:
DoSomeUpdate: DoSomeUpdate:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@v1 uses: actions/checkout@v3
- name: Install or Do Something to Change repository - name: Install or Do Something to Change repository
run: | run: |
echo "This is a new file." >> newfile.txt echo "This is a new file." >> newfile.txt
@@ -18,7 +17,7 @@ jobs:
- name: Checkout New Branch - name: Checkout New Branch
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRANCH_AGAINST: "master" BRANCH_AGAINST: master
run: | run: |
printf "GitHub Actor: ${GITHUB_ACTOR}\n" printf "GitHub Actor: ${GITHUB_ACTOR}\n"
export BRANCH_FROM="update/newfile-$(date '+%Y-%m-%d')" export BRANCH_FROM="update/newfile-$(date '+%Y-%m-%d')"
@@ -43,7 +42,7 @@ jobs:
echo "PULL_REQUEST_FROM_BRANCH=${BRANCH_FROM}" >> $GITHUB_ENV echo "PULL_REQUEST_FROM_BRANCH=${BRANCH_FROM}" >> $GITHUB_ENV
- name: Open Pull Request - name: Open Pull Request
uses: vsoch/pull-request-action@1.0.6 uses: vsoch/pull-request-action@d703f40f3af5ae294f9816395ddf2e3d2d3feafa # 1.0.21
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PULL_REQUEST_BRANCH: "master" PULL_REQUEST_BRANCH: master

View File

@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Generate branch name - name: Generate branch name
uses: actions/github-script@v3 uses: actions/github-script@v6
id: set-branch-name id: set-branch-name
with: with:
script: | script: |
@@ -34,7 +34,7 @@ jobs:
- name: Set branch name - name: Set branch name
run: echo "PULL_REQUEST_TITLE=${{steps.set-branch-name.outputs.result}}" >> $GITHUB_ENV run: echo "PULL_REQUEST_TITLE=${{steps.set-branch-name.outputs.result}}" >> $GITHUB_ENV
- name: Generate PR body - name: Generate PR body
uses: actions/github-script@v3 uses: actions/github-script@v6
id: set-pr-body id: set-pr-body
with: with:
script: | script: |
@@ -47,9 +47,9 @@ jobs:
- name: Set PR body - name: Set PR body
run: echo "PULL_REQUEST_BODY=${{steps.set-pr-body.outputs.result}}" >> $GITHUB_ENV run: echo "PULL_REQUEST_BODY=${{steps.set-pr-body.outputs.result}}" >> $GITHUB_ENV
- name: pull-request-action - name: pull-request-action
uses: vsoch/pull-request-action@1.0.10 uses: vsoch/pull-request-action@d703f40f3af5ae294f9816395ddf2e3d2d3feafa # 1.0.21
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRANCH_PREFIX: "hotfix-" BRANCH_PREFIX: hotfix-
PULL_REQUEST_BRANCH: "production" PULL_REQUEST_BRANCH: production
PULL_REQUEST_REVIEWERS: "AnandChowdhary" PULL_REQUEST_REVIEWERS: AnandChowdhary

View File

@@ -10,11 +10,11 @@ jobs:
steps: steps:
- name: pull-request-action - name: pull-request-action
id: pull_request id: pull_request
uses: vsoch/pull-request-action@1.0.6 uses: vsoch/pull-request-action@d703f40f3af5ae294f9816395ddf2e3d2d3feafa # 1.0.21
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRANCH_PREFIX: "update/" BRANCH_PREFIX: update/
PULL_REQUEST_BRANCH: "master" PULL_REQUEST_BRANCH: master
PULL_REQUEST_REVIEWERS: vsoch PULL_REQUEST_REVIEWERS: vsoch
- name: Test outputs - name: Test outputs
env: env:

View File

@@ -11,8 +11,8 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: pull-request-action - name: pull-request-action
uses: vsoch/pull-request-action@add/support-null-branch uses: vsoch/pull-request-action@d703f40f3af5ae294f9816395ddf2e3d2d3feafa # 1.0.21
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRANCH_PREFIX: "update/" BRANCH_PREFIX: update/
PULL_REQUEST_BRANCH: "master" PULL_REQUEST_BRANCH: master

View File

@@ -18,31 +18,27 @@ jobs:
version=$(echo "$tag_name" | sed 's/^v//') version=$(echo "$tag_name" | sed 's/^v//')
echo "Version: $version" echo "Version: $version"
echo "::set-output name=suite_version::${version}" echo "suite_version=${version}" >> $GITHUB_OUTPUT
echo "::set-output name=suite_update_branch::suite_${version}" echo "suite_update_branch=suite_${version}" >> $GITHUB_OUTPUT
id: data id: data
- name: Permanently save the new suite release - name: Permanently save the new suite release
run: | run: |
mkdir -p releases mkdir -p releases
new_suite_version_yml="releases/suite_${{ steps.data.outputs.suite_version }}.yml" new_suite_version_yml="releases/suite_${{ steps.data.outputs.suite_version }}.yml"
echo "Suite target file: $new_suite_version_yml" echo "Suite target file: $new_suite_version_yml"
cp suite.yml "${new_suite_version_yml}" cp suite.yml "${new_suite_version_yml}"
git add "${new_suite_version_yml}" git add "${new_suite_version_yml}"
git commit -m "Suite v${{ steps.data.outputs.suite_version }} auto-commit of new release files" git commit -m "Suite v${{ steps.data.outputs.suite_version }} auto-commit of new release files"
- name: Push files - name: Push files
run: | run: git push --force "https://${{ github.actor }}:${{secrets.GITHUB_TOKEN}}@github.com/${{ github.repository }}.git" "HEAD:${{ steps.data.outputs.suite_update_branch }}"
git push --force "https://${{ github.actor }}:${{secrets.GITHUB_TOKEN}}@github.com/${{ github.repository }}.git" "HEAD:${{ steps.data.outputs.suite_update_branch }}"
- name: Open a PR to the default branch - name: Open a PR to the default branch
uses: vsoch/pull-request-action@1.0.2 uses: vsoch/pull-request-action@d703f40f3af5ae294f9816395ddf2e3d2d3feafa # 1.0.21
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PULL_REQUEST_FROM_BRANCH: "${{ steps.data.outputs.suite_update_branch }}" PULL_REQUEST_FROM_BRANCH: ${{ steps.data.outputs.suite_update_branch }}
PULL_REQUEST_BRANCH: master PULL_REQUEST_BRANCH: master
PULL_REQUEST_TITLE: "Action: Update suite release file for v${{ steps.data.outputs.suite_version }}" PULL_REQUEST_TITLE: 'Action: Update suite release file for v${{ steps.data.outputs.suite_version }}'
PULL_REQUEST_BODY: "Auto-generated PR!" PULL_REQUEST_BODY: Auto-generated PR!

View File

@@ -9,9 +9,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: pull-request-action - name: pull-request-action
uses: vsoch/pull-request-action@1.0.4 uses: vsoch/pull-request-action@d703f40f3af5ae294f9816395ddf2e3d2d3feafa # 1.0.21
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRANCH_PREFIX: "update/" BRANCH_PREFIX: update/
PULL_REQUEST_BRANCH: "master" PULL_REQUEST_BRANCH: master
PULL_REQUEST_REVIEWERS: vsoch PULL_REQUEST_REVIEWERS: vsoch

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -1,9 +1,10 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import sys
import os
import json import json
import os
import requests import requests
import sys
import time
################################################################################ ################################################################################
# Helper Functions # Helper Functions
@@ -11,6 +12,13 @@ import requests
def get_envar(name): def get_envar(name):
"""
Given a name, return the corresponding environment variable. Exit if not
defined, as using this function indicates the envar is required.
Parameters:
name (str): the name of the environment variable
"""
value = os.environ.get(name) value = os.environ.get(name)
if not value: if not value:
sys.exit("%s is required for vsoch/pull-request-action" % name) sys.exit("%s is required for vsoch/pull-request-action" % name)
@@ -18,6 +26,9 @@ def get_envar(name):
def check_events_json(): def check_events_json():
"""the github events json is required in order to indicate that we are
in an action environment.
"""
events = get_envar("GITHUB_EVENT_PATH") events = get_envar("GITHUB_EVENT_PATH")
if not os.path.exists(events): if not os.path.exists(events):
sys.exit("Cannot find Github events file at ${GITHUB_EVENT_PATH}") sys.exit("Cannot find Github events file at ${GITHUB_EVENT_PATH}")
@@ -25,15 +36,56 @@ def check_events_json():
return events return events
def abort_if_fail(reason): def retry(attempts=5, timeout=30):
"""If PASS_ON_ERROR, don't exit. Otherwise exit with an error and print the reason""" """
A simple retry decorator
We assume rate limiting is in minutes, so we set timeout to 30.
"""
def decorator(func):
def inner(*args, **kwargs):
attempt = 0
while attempt < attempts:
try:
return func(*args, **kwargs)
except Exception as e:
sleep = timeout * 2**attempt
print(f"Retrying in {sleep} seconds - error: {e}")
time.sleep(sleep)
attempt += 1
return func(*args, **kwargs)
return inner
return decorator
def abort_if_fail(response, reason):
"""If PASS_ON_ERROR, don't exit. Otherwise exit with an error and print
the reason.
Parameters:
response (requests.Response) : an unparsed response from requests
reason (str) : a message to print to the user for fail.
"""
message = "%s: %s: %s\n %s" % (
reason,
response.status_code,
response.reason,
response.json(),
)
if os.environ.get("PASS_ON_ERROR"): if os.environ.get("PASS_ON_ERROR"):
print("Error, but PASS_ON_ERROR is set, continuing: %s" % reason) print("Error, but PASS_ON_ERROR is set, continuing: %s" % message)
else: else:
sys.exit(reason) sys.exit(message)
def parse_into_list(values): def parse_into_list(values):
"""
A list of reviewers or assignees to parse from a string to a list
Parameters:
values (str) : a list of space separated, quoted values to parse to a list
"""
if values: if values:
values = values.replace('"', "").replace("'", "") values = values.replace('"', "").replace("'", "")
if not values: if not values:
@@ -41,78 +93,36 @@ def parse_into_list(values):
return [x.strip() for x in values.split(" ")] return [x.strip() for x in values.split(" ")]
def set_env(name, value): def set_env_and_output(name, value):
"""helper function to echo a key/value pair to the environement file""" """
environment_file_path = os.environ.get("GITHUB_ENV") Helper function to echo a key/value pair to the environement file
Parameters:
name (str) : the name of the environment variable
value (str) : the value to write to file
"""
for env_var in ("GITHUB_ENV", "GITHUB_OUTPUT"):
environment_file_path = os.environ.get(env_var)
print("Writing %s=%s to %s" % (name, value, env_var))
with open(environment_file_path, "a") as environment_file: with open(environment_file_path, "a") as environment_file:
environment_file.write("%s=%s" % (name, value)) environment_file.write("%s=%s\n" % (name, value))
################################################################################ @retry(attempts=5, timeout=60)
# Global Variables (we can't use GITHUB_ prefix) def open_pull_request(title, body, target, source, is_draft=False, can_modify=True):
################################################################################ """
Open pull request opens a pull request with a given body and content,
and sets output variables. An unparsed response is returned.
API_VERSION = "v3" Parameters:
BASE = "https://api.github.com" title (str) : the title to set for the new pull request
body (str) : the body to set for the new pull request
HEADERS = { target (str) : the target branch
"Authorization": "token %s" % get_envar("GITHUB_TOKEN"), source (str) : the source branch
"Accept": "application/vnd.github.%s+json;application/vnd.github.antiope-preview+json;application/vnd.github.shadow-cat-preview+json" is_draft (bool) : indicate the pull request is a draft
% API_VERSION, can_modify (bool) : indicate the maintainer can modify
} """
# URLs
REPO_URL = "%s/repos/%s" % (BASE, get_envar("GITHUB_REPOSITORY"))
ISSUE_URL = "%s/issues" % REPO_URL
PULLS_URL = "%s/pulls" % REPO_URL
def create_pull_request(
source,
target,
body,
title,
assignees,
reviewers,
team_reviewers,
is_draft=False,
can_modify=True,
):
# Check if the branch already has a pull request open
params = {"base": target, "head": source, "state": "open"}
data = {"base": target, "head": source, "body": body}
print("Params for checking if pull request exists: %s" % params)
response = requests.get(PULLS_URL, params=params)
# Case 1: 404 might warrant needing a token
if response.status_code == 404:
response = requests.get(PULLS_URL, params=params, headers=HEADERS)
if response.status_code != 200:
abort_if_fail(
"Unable to retrieve information about pull requests: %s: %s"
% (response.status_code, response.reason)
)
response = response.json()
# Option 1: The pull request is already open
is_open = False
if response:
for entry in response:
if entry.get("head", {}).get("ref", "") == source:
print("Pull request from %s to %s is already open!" % (source, target))
is_open = True
# Does the user want to pass if the pull request exists?
if os.environ.get("PASS_IF_EXISTS"):
print("PASS_IF_EXISTS is set, exiting with success status.")
sys.exit(0)
break
# Option 2: Open a new pull request
if not is_open:
print("No pull request from %s to %s is open, continuing!" % (source, target)) print("No pull request from %s to %s is open, continuing!" % (source, target))
# Post the pull request # Post the pull request
@@ -127,15 +137,49 @@ def create_pull_request(
print("Data for opening pull request: %s" % data) print("Data for opening pull request: %s" % data)
response = requests.post(PULLS_URL, json=data, headers=HEADERS) response = requests.post(PULLS_URL, json=data, headers=HEADERS)
if response.status_code != 201: if response.status_code != 201:
abort_if_fail( print(f"pull request url is {PULLS_URL}")
"Unable to create pull request: %s: %s, %s" abort_if_fail(response, "Unable to create pull request")
% (
response.status_code,
response.reason,
response.json().get("message", ""),
)
)
return response
@retry()
def update_pull_request(entry, title, body, target, state=None):
"""
Given an existing pull request, update it.
Parameters:
entry (dict) : the pull request metadata
title (str) : the title to set for the new pull request
body (str) : the body to set for the new pull request
target (str) : the target branch
state (bool) : the state of the PR (open, closed)
"""
print("PULL_REQUEST_UPDATE is set, updating existing pull request.")
data = {
"title": title,
"body": body,
"base": target,
"state": state or "open",
}
# PATCH /repos/{owner}/{repo}/pulls/{pull_number}
url = "%s/%s" % (PULLS_URL, entry.get("number"))
print("Data for updating pull request: %s" % data)
response = requests.patch(url, json=data, headers=HEADERS)
if response.status_code != 200:
abort_if_fail(response, "Unable to create pull request")
return response
def set_pull_request_groups(response):
"""
Given a response for an open or updated PR, set metadata
Parameters:
response (requests.Response) : a requests response, unparsed
"""
# Expected return codes are 0 for success # Expected return codes are 0 for success
pull_request_return_code = ( pull_request_return_code = (
0 if response.status_code == 201 else response.status_code 0 if response.status_code == 201 else response.status_code
@@ -147,23 +191,49 @@ def create_pull_request(
number = response.get("number") number = response.get("number")
html_url = response.get("html_url") html_url = response.get("html_url")
print("Number opened for PR is %s" % number) print("Number opened for PR is %s" % number)
set_env("PULL_REQUEST_NUMBER", number) set_env_and_output("PULL_REQUEST_NUMBER", number)
print("::set-output name=pull_request_number::%s" % number) set_env_and_output("PULL_REQUEST_RETURN_CODE", pull_request_return_code)
set_env("PULL_REQUEST_RETURN_CODE", pull_request_return_code) set_env_and_output("PULL_REQUEST_URL", html_url)
print(
"::set-output name=pull_request_return_code::%s" % pull_request_return_code
)
set_env("PULL_REQUEST_URL", html_url)
print("::set-output name=pull_request_url::%s" % html_url)
if assignees:
@retry(attempts=5, timeout=30)
def list_pull_requests(target, source):
"""Given a target and source, return a list of pull requests that match
(or simply exit given some kind of error code)
Parameters:
target (str) : the target branch
source (str) : the source branch
"""
# Check if the branch already has a pull request open
params = {"base": target, "head": source, "state": "open"}
print("Params for checking if pull request exists: %s" % params)
response = requests.get(PULLS_URL, params=params)
# Case 1: 401, 404 might warrant needing a token
if response.status_code in [401, 404]:
response = requests.get(PULLS_URL, params=params, headers=HEADERS)
if response.status_code != 200:
abort_if_fail(response, "Unable to retrieve information about pull requests")
return response.json()
@retry(attempts=5, timeout=30)
def add_assignees(entry, assignees):
"""
Given a pull request metadata (from create or update) add assignees
Parameters:
entry (dict) : the pull request metadata
assignees (str) : comma separated assignees string set by action
"""
# Remove leading and trailing quotes # Remove leading and trailing quotes
assignees = parse_into_list(assignees) assignees = parse_into_list(assignees)
number = entry.get("number")
print( print(
"Attempting to assign %s to pull request with number %s" "Attempting to assign %s to pull request with number %s" % (assignees, number)
% (assignees, number)
) )
# POST /repos/:owner/:repo/issues/:issue_number/assignees # POST /repos/:owner/:repo/issues/:issue_number/assignees
@@ -171,62 +241,149 @@ def create_pull_request(
ASSIGNEES_URL = "%s/%s/assignees" % (ISSUE_URL, number) ASSIGNEES_URL = "%s/%s/assignees" % (ISSUE_URL, number)
response = requests.post(ASSIGNEES_URL, json=data, headers=HEADERS) response = requests.post(ASSIGNEES_URL, json=data, headers=HEADERS)
if response.status_code != 201: if response.status_code != 201:
abort_if_fail( abort_if_fail(response, "Unable to create assignees")
"Unable to create assignees: %s: %s, %s"
% (
response.status_code,
response.reason,
response.json().get("message", ""),
)
)
assignees_return_code = ( assignees_return_code = 0 if response.status_code == 201 else response.status_code
0 if response.status_code == 201 else response.status_code print("::group::github assignees response")
) print(response.json())
set_env("ASSIGNEES_RETURN_CODE", assignees_return_code) print("::endgroup::github assignees response")
print("::set-output name=assignees_return_code::%s" % assignees_return_code) set_env_and_output("ASSIGNEES_RETURN_CODE", assignees_return_code)
if reviewers or team_reviewers:
print( def find_pull_request(listing, source):
"Found reviewers: %s and team reviewers: %s" """
% (reviewers, team_reviewers) Given a listing and a source, find a pull request based on the source
) (the branch name).
Parameters:
listing (list) : the list of PR objects (dict) to parse over
source (str) : the source (head) branch to look for
"""
if listing:
for entry in listing:
if entry.get("head", {}).get("ref", "") == source:
print("Pull request from %s is already open!" % source)
return entry
@retry(attempts=5, timeout=30)
def find_default_branch():
"""
Find default branch for a repo (only called if branch not provided)
"""
response = requests.get(REPO_URL)
# Case 1: 401, 404 might need a token
if response.status_code in [401, 404]:
response = requests.get(REPO_URL, headers=HEADERS)
if response.status_code != 200:
abort_if_fail(response, "Unable to retrieve default branch")
default_branch = response.json()["default_branch"]
print("Found default branch: %s" % default_branch)
return default_branch
@retry(attempts=5, timeout=30)
def add_reviewers(entry, reviewers, team_reviewers):
"""
Given regular or team reviewers, add them to a PR.
Parameters:
entry (dict) : the pull request metadata
"""
print("Found reviewers: %s and team reviewers: %s" % (reviewers, team_reviewers))
team_reviewers = parse_into_list(team_reviewers) team_reviewers = parse_into_list(team_reviewers)
reviewers = parse_into_list(reviewers) reviewers = parse_into_list(reviewers)
print( print("Parsed reviewers: %s and team reviewers: %s" % (reviewers, team_reviewers))
"Parsed reviewers: %s and team reviewers: %s"
% (reviewers, team_reviewers)
)
# POST /repos/:owner/:repo/pulls/:pull_number/requested_reviewers # POST /repos/:owner/:repo/pulls/:pull_number/requested_reviewers
REVIEWERS_URL = "%s/%s/requested_reviewers" % (PULLS_URL, number) REVIEWERS_URL = "%s/%s/requested_reviewers" % (PULLS_URL, entry.get("number"))
data = {"reviewers": reviewers, "team_reviewers": team_reviewers} data = {"reviewers": reviewers, "team_reviewers": team_reviewers}
response = requests.post(REVIEWERS_URL, json=data, headers=HEADERS) response = requests.post(REVIEWERS_URL, json=data, headers=HEADERS)
if response.status_code != 201: if response.status_code != 201:
abort_if_fail( abort_if_fail(response, "Unable to assign reviewers")
"Unable to assign reviewers: %s: %s, %s" reviewers_return_code = 0 if response.status_code == 201 else response.status_code
% (
response.status_code,
response.reason,
response.json().get("message", ""),
)
)
reviewers_return_code = (
0 if response.status_code == 201 else response.status_code
)
response = response.json()
print("::group::github reviewers response") print("::group::github reviewers response")
print(response) print(response.json())
print("::endgroup::github reviewers response") print("::endgroup::github reviewers response")
set_env("REVIEWERS_RETURN_CODE", reviewers_return_code) set_env_and_output("REVIEWERS_RETURN_CODE", reviewers_return_code)
print("::set-output name=reviewers_return_code::%s" % reviewers_return_code)
print("Add reviewers return code: %s" % reviewers_return_code)
################################################################################
# Global Variables (we can't use GITHUB_ prefix)
################################################################################
API_VERSION = "v3"
# Allow for a GitHub enterprise URL
BASE = os.environ.get("GITHUB_API_URL") or "https://api.github.com"
PR_TOKEN = os.environ.get("PULL_REQUEST_TOKEN") or get_envar("GITHUB_TOKEN")
PR_REPO = os.environ.get("PULL_REQUEST_REPOSITORY") or get_envar("GITHUB_REPOSITORY")
HEADERS = {
"Authorization": "token %s" % PR_TOKEN,
"Accept": "application/vnd.github.%s+json;application/vnd.github.antiope-preview+json;application/vnd.github.shadow-cat-preview+json"
% API_VERSION,
}
# URLs
REPO_URL = "%s/repos/%s" % (BASE, PR_REPO)
ISSUE_URL = "%s/issues" % REPO_URL
PULLS_URL = "%s/pulls" % REPO_URL
def create_pull_request(
source,
target,
body,
title,
assignees,
reviewers,
team_reviewers,
is_draft=False,
can_modify=True,
state="open",
):
"""Create pull request is the base function that determines if the PR exists,
and then updates or creates it depending on user preferences.
"""
listing = list_pull_requests(target, source)
# Determine if the pull request is already open
entry = find_pull_request(listing, source)
response = None
# Case 1: we found the PR, the user wants to pass
if entry and os.environ.get("PASS_IF_EXISTS"):
print("PASS_IF_EXISTS is set, exiting with success status.")
sys.exit(0)
# Does the user want to update the existing PR?
if entry and os.environ.get("PULL_REQUEST_UPDATE"):
response = update_pull_request(entry, title, body, target, state)
set_pull_request_groups(response)
# If it's not open, we open a new pull request
elif not entry:
response = open_pull_request(title, body, target, source, is_draft, can_modify)
set_pull_request_groups(response)
# If we have a response, parse into json (no longer need retvals)
response = response.json() if response else None
# If we have opened or updated, we can add assignees
if response and assignees:
add_assignees(response, assignees)
if response and (reviewers or team_reviewers):
add_reviewers(response, reviewers, team_reviewers)
def main(): def main():
"""main primarily parses environment variables to prepare for creation"""
# path to file that contains the POST response of the event # path to file that contains the POST response of the event
# Example: https://github.com/actions/bin/tree/master/debug # Example: https://github.com/actions/bin/tree/master/debug
@@ -238,8 +395,11 @@ def main():
if not branch_prefix: if not branch_prefix:
print("No branch prefix is set, all branches will be used.") print("No branch prefix is set, all branches will be used.")
# Default to master to support older, will eventually change to main # Default to project default branch if none provided
pull_request_branch = os.environ.get("PULL_REQUEST_BRANCH", "master") pull_request_branch = os.environ.get("PULL_REQUEST_BRANCH")
if not pull_request_branch:
pull_request_branch = find_default_branch()
print("Pull requests will go to %s" % pull_request_branch) print("Pull requests will go to %s" % pull_request_branch)
# Pull request draft # Pull request draft
@@ -248,21 +408,22 @@ def main():
print("No explicit preference for draft PR: created PRs will be normal PRs.") print("No explicit preference for draft PR: created PRs will be normal PRs.")
pull_request_draft = False pull_request_draft = False
else: else:
print( print("PULL_REQUEST_DRAFT set to a value: created PRs will be draft PRs.")
"Environment variable PULL_REQUEST_DRAFT set to a value: created PRs will be draft PRs."
)
pull_request_draft = True pull_request_draft = True
# If an update is true, we can change the state
pull_request_state = os.environ.get("PULL_REQUEST_STATE", "open")
if pull_request_state not in ["open", "closed"]:
sys.exit("State is required to be one of 'open' or 'closed'")
# Maintainer can modify, defaults to CAN, unless user sets MAINTAINER_CANT_MODIFY # Maintainer can modify, defaults to CAN, unless user sets MAINTAINER_CANT_MODIFY
maintainer_can_modify = os.environ.get("MAINTAINER_CANT_MODIFY") maintainer_can_modify = os.environ.get("MAINTAINER_CANT_MODIFY")
if not maintainer_can_modify: if not maintainer_can_modify:
print( print("No preference for maintainer being able to modify: default is true.")
"No explicit preference for maintainer being able to modify: default is true."
)
maintainer_can_modify = True maintainer_can_modify = True
else: else:
print( print(
"Environment variable MAINTAINER_CANT_MODIFY set to a value: maintainer will not be able to modify." "MAINTAINER_CANT_MODIFY set to a value: maintainer will not be able to modify."
) )
maintainer_can_modify = False maintainer_can_modify = False
@@ -292,7 +453,7 @@ def main():
if not from_branch: if not from_branch:
print("PULL_REQUEST_FROM_BRANCH is not set, checking branch in payload.") print("PULL_REQUEST_FROM_BRANCH is not set, checking branch in payload.")
with open(check_events_json(), "r") as fd: with open(check_events_json(), "r") as fd:
from_branch = json.loads(fd.read()).get("ref") from_branch = json.loads(fd.read()).get("ref", "")
from_branch = from_branch.replace("refs/heads/", "").strip("/") from_branch = from_branch.replace("refs/heads/", "").strip("/")
else: else:
print("PULL_REQUEST_FROM_BRANCH is set.") print("PULL_REQUEST_FROM_BRANCH is set.")
@@ -302,23 +463,20 @@ def main():
print("Found branch %s to open PR from" % from_branch) print("Found branch %s to open PR from" % from_branch)
else: else:
sys.exit( sys.exit(
"No branch in payload, you are required to define PULL_REQUEST_FROM_BRANCH in the environment." "You are required to define PULL_REQUEST_FROM_BRANCH in the environment."
) )
# If it's to the target branch, ignore it # If it's to the target branch, ignore it
if from_branch == pull_request_branch: if from_branch == pull_request_branch:
print("Target and current branch are identical (%s), skipping." % from_branch) print("Target and current branch are identical (%s), skipping." % from_branch)
else: sys.exit(0)
# If the prefix for the branch matches # If the prefix for the branch matches
if not branch_prefix or from_branch.startswith(branch_prefix): if not branch_prefix or from_branch.startswith(branch_prefix):
# Pull request body (optional) # Pull request body (optional)
pull_request_body = os.environ.get( pull_request_body = os.environ.get(
"PULL_REQUEST_BODY", "PULL_REQUEST_BODY",
"This is an automated pull request to update from branch %s" "This is an automated pull request to update from branch %s" % from_branch,
% from_branch,
) )
print("::group::pull request body") print("::group::pull request body")
@@ -344,6 +502,7 @@ def main():
assignees=assignees, assignees=assignees,
reviewers=reviewers, reviewers=reviewers,
team_reviewers=team_reviewers, team_reviewers=team_reviewers,
state=pull_request_state,
) )