From b6570211b656b46b51b5a8cba58cfe5fa137f3f4 Mon Sep 17 00:00:00 2001 From: Mark Bestavros Date: Wed, 26 May 2021 20:12:04 -0400 Subject: [PATCH] Implement GITHUB_TOKEN interoperability; accept tokens as inputs instead of env variables --- README.md | 122 ++++++++++++++++++++++++----------------- action.yml | 17 ++++-- src/assign | 11 ++-- src/copy-labels-linked | 9 ++- src/merge | 11 ++-- src/request | 12 ++-- 6 files changed, 113 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index b96fcfe..0d180d9 100644 --- a/README.md +++ b/README.md @@ -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: @@ -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 @@ -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 @@ -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? @@ -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. diff --git a/action.yml b/action.yml index d512609..8864202 100644 --- a/action.yml +++ b/action.yml @@ -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 @@ -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" diff --git a/src/assign b/src/assign index 4d05fce..8f1d7eb 100755 --- a/src/assign +++ b/src/assign @@ -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) @@ -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)) @@ -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() diff --git a/src/copy-labels-linked b/src/copy-labels-linked index d566047..4d87bed 100755 --- a/src/copy-labels-linked +++ b/src/copy-labels-linked @@ -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: @@ -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() diff --git a/src/merge b/src/merge index 2302ebb..50096c5 100755 --- a/src/merge +++ b/src/merge @@ -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: @@ -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']}" @@ -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() diff --git a/src/request b/src/request index 9ba68b4..5afaebd 100755 --- a/src/request +++ b/src/request @@ -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. @@ -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()