Skip to content

Commit

Permalink
Implement GITHUB_TOKEN interoperability; accept tokens as inputs inst…
Browse files Browse the repository at this point in the history
…ead of env variables
  • Loading branch information
mbestavros committed May 27, 2021
1 parent f336494 commit b657021
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 69 deletions.
122 changes: 73 additions & 49 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@

Each of these actions are discrete, and can be enabled separately if desired.

### `assign`: Automatically adjust PR assignees based on who is most responsible for pushing it forward

The heart and soul of the toolset: this action will automatically adjust assignees on a pull request such that the current assignees are the ones that must take action to move the pull request forward. Here's an example timeline of actions for a PR submitted by author X:

- PR from author X arrives. Any requested reviewers (let's say reviewers A, B, and C) are assigned.
- Reviewer A leaves a review requesting changes. Reviewers A, B, and C are unassigned, and author X is assigned to address A's review.
- X makes changes to the PR and re-requests review from reviewer A. Since there are no outstanding Changes Requested reviews, all requested reviewers that haven't yet approved (A, B, C) are put back on the PR.
- Reviewer B approves the PR, and is unassigned. Reviewers A and C are still assigned.
- Reviewer C approves the PR, and is unassigned. Reviewer A is still assigned.
- etc...

In effect, this turns Github's Assignee field from a vague "point person" to a clear indicator of responsibility.

### `request`: Automatically request reviewers on new pull requests

When a new PR arrives, this action will request a (configurable) number of reviewers, pulling from a mixture of:
Expand All @@ -20,20 +33,7 @@ Inputs:
- `reviewers`: the team name to pull reviewers from in the parent repo's organization
- `num_to_request`: the number of reviewers to request on new PRs

You can see an example of how inputs should be specified in the example workflow below.

### `assign`: Automatically adjust PR assignees based on who is most responsible for pushing it forward

The heart and soul of the toolset: this action will automatically adjust assignees on a pull request such that the current assignees are the ones that must take action to move the pull request forward. Here's an example timeline of actions for a PR submitted by author X:

- PR from author X arrives. Any requested reviewers (let's say reviewers A, B, and C) are assigned.
- Reviewer A leaves a review requesting changes. Reviewers A, B, and C are unassigned, and author X is assigned to address A's review.
- X makes changes to the PR and re-requests review from reviewer A. Since there are no outstanding Changes Requested reviews, all requested reviewers that haven't yet approved (A, B, C) are put back on the PR.
- Reviewer B approves the PR, and is unassigned. Reviewers A and C are still assigned.
- Reviewer C approves the PR, and is unassigned. Reviewer A is still assigned.
- etc...

In effect, this turns Github's Assignee field from a vague "point person" to a clear indicator of responsibility.
You can see an example of how inputs should be specified in the Usage section below.

### `copy-labels-linked`: Copy any labels present on linked issues to PRs

Expand All @@ -45,26 +45,9 @@ When a pull request meets a repo's configured criteria for [mergeability](https:

## Usage

Setup takes just a few straightforward steps.

### Authenticating with Github

Many `actions-automation` actions rely on a [personal access token](https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/creating-a-personal-access-token)
to interact with Github and work their magic. If you're setting up a new
repository for use with `actions-automation` tools, you'll need to provision one with the `public_repo`, `read:org`, and `write:org`
scopes enabled and make it available as a [shared secret](https://docs.github.com/en/free-pro-team@latest/actions/reference/encrypted-secrets)
to your repository, using the name `BOT_TOKEN`. (Organization-wide secrets also work.)

Github _strongly_
[recommends](https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/security-hardening-for-github-actions#considering-cross-repository-access)
creating a separate account for PATs like this, and scoping access to that
account accordingly.

### Enabling `pull-request-responsibility` on a repository
In most cases, all you need to start using `actions-automation` is a single workflow!

Once a token is available to your repo, the final step is to enable `pull-request-responsibility` via
Github Actions. To do so, create a new workflow under `.github/workflows` with
the following structure:
Create a new workflow `yml` under `.github/workflows` with the following structure:

```yml
name: pull-request-responsibility
Expand Down Expand Up @@ -115,23 +98,73 @@ on:
jobs:
pull-request-responsibility:
runs-on: ubuntu-latest
env:
BOT_TOKEN: ${{ secrets.BOT_TOKEN }}
name: pull-request-responsibility
steps:
- uses: actions-automation/pull-request-responsibility@main
with:
actions: "request,assign,copy-labels-linked,merge" # The actions to run.
reviewers: "reviews" # The team to pull reviewers from for `request`.
num_to_request: 3 # The number of reviewers that `request` should request on new PRs.

actions: "assign,copy-labels-linked,merge" # The actions to run.
token: ${{ secrets.GITHUB_TOKEN }}
```
Note that you'll need to configure the action's inputs accordingly. If none are provided, only the `assign` action will run by default; the rest are opt-in. The other parameters are required for the `request` action, if enabled.
Note that you'll need to configure the action's inputs accordingly. If none are provided, only the `assign` action will run by default; the rest are opt-in via the `actions` input. Additionally, you'll need to provide a Github access token; the `GITHUB_TOKEN` should suffice for most cases.

Once this is in place, you're done! `pull-request-responsibility` should begin working on your repo
when certain events (ex. an issue/PR gets opened) happen.

### Enabling the `request` action

You may have noticed that the `request` action is not included in the workflow above. That's because it performs cross-repo API calls and needs some additional setup.

### Github authentication with a personal access token

The Actions-provided `GITHUB_TOKEN` secret is good for most of what `pull-request-responsibility` does, but it does not have permission to read or write data from outside the repository that spawned it. This poses a problem with the `request` action, which looks at things like user availability and organization-level teams.

In order for `request` to function correctly, we'll need to authenticate with Github using a **personal access token** instead of the `GITHUB_TOKEN`. These are user-generated and effectively allow scripts to act as a user through the Github API. You can manage your active tokens [here](https://github.com/settings/tokens).

Provision one with the `public_repo`, `read:org`, and `write:org` scopes enabled and make it available as a [shared secret](https://docs.github.com/en/free-pro-team@latest/actions/reference/encrypted-secrets)
to your repository, using the name `BOT_TOKEN`. (Organization-wide secrets also work.)

Github _strongly_
[recommends](https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/security-hardening-for-github-actions#considering-cross-repository-access)
creating a separate account for PATs like this, and scoping access to that
account accordingly. It's recommended to heed their advice; PATs are dangerous if leaked, and can result in malicious actors abusing the account that created it.

If you're using any other `actions-automation` tools, chances are this step will be necessary as well. Once the token is available as a secret, however, it should be straightforward to use it in perpetuity.

### Enabling `request` in your workflow

With the repository secret available, replace the `token` input with your new PAT:

```yml
token: ${{ secrets.BOT_TOKEN }}
```

You'll also need to provide some additional inputs for the action to work (documented in its description):

```yml
reviewers: "reviews" # The team to pull reviewers from for `request`.
num_to_request: 3 # The number of reviewers that `request` should request on new PRs.
```
And, finally, enable the action in the `actions` input:

```yml
actions: "request,assign,copy-labels-linked,merge" # The actions to run.
```

Your completed step should look something like this:

```yml
- uses: actions-automation/pull-request-responsibility@main
with:
actions: "request,assign,copy-labels-linked,merge" # The actions to run.
token: ${{ secrets.BOT_TOKEN }}
reviewers: "reviews" # The team to pull reviewers from for `request`.
num_to_request: 3 # The number of reviewers that `request` should request on new PRs.
```
And you should be done!
## FAQ
### Why do I need to listen for every single Actions event in my workflow?
Expand All @@ -144,15 +177,6 @@ The action doesn't _need_ every trigger to work properly -- in fact, it doesn't
use most of them at all. However, there's little downside to enabling all of
them, and doing so avoids the need to change workflows if new functionality using a new trigger is added.

### What happens if I don't set `BOT_TOKEN` properly?

If `pull-request-responsibility` can't find a valid token under `BOT_TOKEN`, it will fail gracefully
with a note reminding you to add one if you want to opt into automation. It
should _not_ send you a failure email about it.

This avoids forks of repositories that have `pull-request-responsibility` enabled from spamming their
authors with needless Actions failure emails about improperly-set credentials.

## Credits

This action was originally developed for the Enarx project as part of [Enarxbot](https://github.com/enarx/bot), and is now available for any repo to use here.
17 changes: 13 additions & 4 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ inputs:
Default: "assign"
required: false
default: 'assign'
token:
description: >
A Github access token. Needed in order to read data from and write to
Github.
In most cases, the GITHUB_TOKEN will suffice; however, to use the full
feature set of the "request" action, a properly-scoped PAT is required.
required: false
default: ''
reviewers:
description: >
The Github team to pull reviewers from. Must be a valid team name from the
Expand All @@ -41,22 +50,22 @@ runs:
shell: bash
- run: >
if [[ "${{ inputs.actions }}" =~ (^|,)"merge"(,|$) ]]; then
echo " --- MERGE --- " && $GITHUB_ACTION_PATH/src/merge
echo " --- MERGE --- " && $GITHUB_ACTION_PATH/src/merge "${{ inputs.token }}"
fi
shell: bash
- run: >
if [[ "${{ inputs.actions }}" =~ (^|,)"copy-labels-linked"(,|$) ]]; then
echo " --- COPY-LABELS-LINKED --- " && $GITHUB_ACTION_PATH/src/copy-labels-linked
echo " --- COPY-LABELS-LINKED --- " && $GITHUB_ACTION_PATH/src/copy-labels-linked "${{ inputs.token }}"
fi
shell: bash
- run: >
if [[ "${{ inputs.actions }}" =~ (^|,)"request"(,|$) ]]; then
echo " --- REQUEST --- " && $GITHUB_ACTION_PATH/src/request "${{ inputs.reviewers }}" "${{ inputs.num_to_request }}"
echo " --- REQUEST --- " && $GITHUB_ACTION_PATH/src/request "${{ inputs.token }}" "${{ inputs.reviewers }}" "${{ inputs.num_to_request }}"
fi
shell: bash
- run: >
if [[ "${{ inputs.actions }}" =~ (^|,)"assign"(,|$) ]]; then
echo " --- ASSIGN --- " && $GITHUB_ACTION_PATH/src/assign
echo " --- ASSIGN --- " && $GITHUB_ACTION_PATH/src/assign "${{ inputs.token }}"
fi
shell: bash
- run: bash -c "env | sort"
Expand Down
11 changes: 7 additions & 4 deletions src/assign
Original file line number Diff line number Diff line change
Expand Up @@ -141,11 +141,10 @@ def get_responsible(pr):
continue
break

def update_assignees(number_to_update=None):
def update_assignees(token, number_to_update=None):
"Updates assignees on all repo PRs by default, or a specified PR if input."

org, repo = os.environ["GITHUB_REPOSITORY"].split("/")
token = os.environ.get('BOT_TOKEN', None)

try:
pr_data = githubgql.graphql(QUERY, token=token, org=org, repo=repo, cursor=QUERY_CURSORS)
Expand All @@ -158,7 +157,7 @@ def update_assignees(number_to_update=None):

for pr in pr_data['repository']['pullRequests']['nodes']:
if number_to_update not in [pr['number'], None]:
continue
continue
slug = f"{org}/{repo}#{pr['number']}"
try:
responsible = set(get_responsible(pr))
Expand Down Expand Up @@ -217,7 +216,11 @@ def main():
if os.environ["GITHUB_EVENT_NAME"] not in ["schedule", "workflow_dispatch"]:
sys.exit(0)

update_assignees()
token = sys.argv[1]
if len(token) == 0:
token = os.environ.get('BOT_TOKEN', None)

update_assignees(token=token)

if __name__ == "__main__":
main()
9 changes: 6 additions & 3 deletions src/copy-labels-linked
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,8 @@ def get_related_issues(body, commits):
for verb, num in regex.findall(c["commit"]["message"]):
yield int(num)

def copy_labels_linked(id):
def copy_labels_linked(token, id):
owner, repo = os.environ["GITHUB_REPOSITORY"].split("/")
token = os.environ.get('BOT_TOKEN', None)

# Get PR data and open issues in the repo.
try:
Expand Down Expand Up @@ -164,9 +163,13 @@ def main():
if event["action"] not in {"opened", "reopened"}:
sys.exit(0)

token = sys.argv[1]
if len(token) == 0:
token = os.environ.get('BOT_TOKEN', None)

id = event['pull_request']['node_id']

copy_labels_linked(id)
copy_labels_linked(token=token, id=id)

if __name__ == "__main__":
main()
11 changes: 7 additions & 4 deletions src/merge
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,8 @@ mutation($input: MergePullRequestInput!) {
}
'''

def merge(number_to_merge=None):
def merge(token, number_to_merge=None):
owner, repo = os.environ["GITHUB_REPOSITORY"].split("/")
token = os.environ.get('BOT_TOKEN', None)
cursors = {"cursor": ["repository", "pullRequests"]}

try:
Expand All @@ -51,7 +50,7 @@ def merge(number_to_merge=None):

for (id, pr) in prs.items():
if number_to_merge not in [pr['number'], None]:
continue
continue
# Status strings.
merged_indicator = "✗"
pr_identifier = f"{owner}/{repo}#{pr['number']}"
Expand All @@ -75,7 +74,11 @@ def main():
if os.environ["GITHUB_EVENT_NAME"] != "schedule":
sys.exit(0)

merge()
token = sys.argv[1]
if len(token) == 0:
token = os.environ.get('BOT_TOKEN', None)

merge(token=token)

if __name__ == "__main__":
main()
12 changes: 7 additions & 5 deletions src/request
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,7 @@ def iterusers(nested_dictionary):
elif type(value) is dict:
yield from iterusers(value)

def auto_request(pr, org, token):
team_name = sys.argv[1]
num_to_request = sys.argv[2]
def auto_request(pr, org, token, team_name="", num_to_request=""):

# Inputs are optional and default to empty strings; check if they're populated
# and fail if not.
Expand Down Expand Up @@ -195,9 +193,13 @@ def main():
# Extract relevant data from event/environment.
pr = event['pull_request']['node_id']
org = event['organization']['node_id']
token = os.environ.get('BOT_TOKEN', None)
token = sys.argv[1]
if len(token) == 0:
token = os.environ.get('BOT_TOKEN', None)
team_name = sys.argv[2]
num_to_request = sys.argv[3]

auto_request(pr, org, token)
auto_request(pr=pr, org=org, token=token, team_name=team_name, num_to_request=num_to_request)

if __name__ == "__main__":
main()

0 comments on commit b657021

Please sign in to comment.