Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GH-33977: [Dev] PR Workflow automation bot #34161

Merged
merged 16 commits into from
Feb 28, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .github/workflows/pr_bot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

name: pr-workflow-bot
raulcd marked this conversation as resolved.
Show resolved Hide resolved
on:
pull_request:
raulcd marked this conversation as resolved.
Show resolved Hide resolved
types:
- opened
- converted_to_draft
- ready_for_review
pull_request_review:
types:
- submitted
push:
raulcd marked this conversation as resolved.
Show resolved Hide resolved

raulcd marked this conversation as resolved.
Show resolved Hide resolved
jobs:
pr-workflow-bot-job:
name: "PR Workflow bot"
permissions:
contents: read
pull-requests: write
issues: write
raulcd marked this conversation as resolved.
Show resolved Hide resolved
runs-on: ubuntu-latest
steps:
- name: Checkout Arrow
uses: actions/checkout@v3
with:
path: arrow
raulcd marked this conversation as resolved.
Show resolved Hide resolved
# fetch the tags for version number generation
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: 3.8
- name: Install Archery and Crossbow dependencies
run: pip install -e arrow/dev/archery[bot]
- name: Handle PR workflow event
env:
ARROW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROSSBOW_GITHUB_TOKEN: ${{ secrets.CROSSBOW_GITHUB_TOKEN }}
raulcd marked this conversation as resolved.
Show resolved Hide resolved
run: |
archery trigger-bot \
--event-name ${{ github.event_name }} \
--event-payload ${{ github.event_path }}
142 changes: 141 additions & 1 deletion dev/archery/archery/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@
# specific language governing permissions and limitations
# under the License.

import enum
import os
import shlex
from pathlib import Path
from functools import partial
from functools import lru_cache, partial
import tempfile

import click
Expand All @@ -29,6 +30,10 @@
from .crossbow import Repo, Queue, Config, Target, Job, CommentReport


def cached_property(fn):
return property(lru_cache(maxsize=1)(fn))


class EventError(Exception):
pass

Expand Down Expand Up @@ -82,6 +87,141 @@ def parse_args(self, ctx, args):
group = partial(click.group, cls=Group)


LABEL_PREFIX = "awaiting"


@enum.unique
class PullRequestState(enum.Enum):
"""State of a pull request."""

review = f"{LABEL_PREFIX} review"
committer_review = f"{LABEL_PREFIX} committer review"
changes = f"{LABEL_PREFIX} changes"
change_review = f"{LABEL_PREFIX} change review"
merge = f"{LABEL_PREFIX} merge"


COMMITTER_ROLES = {'OWNER', 'MEMBER'}


class PullRequestWorkflowBot:

def __init__(self, event_name, event_payload, token=None):
self.github = github.Github(token)
self.event_name = event_name
self.event_payload = event_payload

@cached_property
def pull(self):
"""
Returns a github.PullRequest object associated with the event.
In case of a commit we search the PR associated with the commit.
"""
if self.event_name == 'push':
return self._get_pr_for_commit()
else:
return self.repo.get_pull(self.event_payload['pull_request']['number'])

@cached_property
def repo(self):
return self.github.get_repo(self.event_payload['repository']['id'], lazy=True)

def handle(self):
current_state = None
try:
current_state = self.get_current_state()
except EventError:
# In case of error (more than one state) we clear state labels
# only possible if a label has been manually added.
self.clear_current_state()
new_state = self.get_target_state(current_state)
if current_state != new_state.value:
if current_state:
self.clear_current_state()
self.set_state(new_state)

def get_current_state(self):
"""
Returns a string with the current PR state label
raulcd marked this conversation as resolved.
Show resolved Hide resolved
based on label starting with LABEL_PREFIX.
If more than one label is found raises EventError.
If no label is found returns None.
"""
states = [label.name for label in self.pull.get_labels()
if label.name.startswith(LABEL_PREFIX)]
if len(states) > 1:
raise EventError(f"PR cannot be on more than one states - {states}")
elif states:
return states[0]

def clear_current_state(self):
"""
Removes all existing labels starting with LABEL_PREFIX
"""
for label in self.pull.get_labels():
if label.name.startswith(LABEL_PREFIX):
self.pull.remove_from_labels(label)

def get_target_state(self, current_state):
raulcd marked this conversation as resolved.
Show resolved Hide resolved
"""
Returns the expected target state based on the event and
the current state.
"""
if (self.event_name == "pull_request" and
self.event_payload['action'] == 'opened'):
if (self.event_payload['pull_request']['author_association'] in
COMMITTER_ROLES):
return PullRequestState.committer_review
else:
return PullRequestState.review
elif (self.event_name == "pull_request_review" and
self.event_payload["action"] == "submitted"):
review_state = self.event_payload["review"]["state"].lower()
is_committer_review = (self.event_payload['review']['author_association']
in COMMITTER_ROLES)
if not is_committer_review:
# Non-committer reviews cannot change state once committer has already
# reviewed and requested changes.
if current_state in (
PullRequestState.change_review.value,
PullRequestState.changes.value):
return PullRequestState(current_state)
else:
return PullRequestState.committer_review
if review_state == 'approved':
return PullRequestState.merge
elif review_state in ("changes_requested", "commented"):
raulcd marked this conversation as resolved.
Show resolved Hide resolved
return PullRequestState.changes
elif (self.event_name == "push" and
current_state == PullRequestState.changes.value):
return PullRequestState.change_review
# Default already opened PRs to Review state.
if current_state is None:
current_state = PullRequestState.review.value
return PullRequestState(current_state)

def set_state(self, state):
"""Sets the State label to the PR."""
self.pull.add_to_labels(state.value)

def _get_pr_for_commit(self):
"""Find the PR containing the specific commit hash."""
sha = self.event_payload['commits'][-1]['id']
repo = self.event_payload['repository']['full_name']
prs_for_commit = self.github.search_issues(
"",
qualifiers={"type": "pr",
"repo": "apache/arrow",
"sha": sha}
)
pull = None
try:
pull = self.repo.get_pull(prs_for_commit[0].number)
except IndexError:
pass
return pull


class CommentBot:

def __init__(self, name, handler, token=None):
Expand Down
11 changes: 7 additions & 4 deletions dev/archery/archery/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -781,12 +781,15 @@ def integration(with_all=False, random_seed=12345, **args):
@click.option('--arrow-token', envvar='ARROW_GITHUB_TOKEN',
help='OAuth token for responding comment in the arrow repo')
def trigger_bot(event_name, event_payload, arrow_token):
from .bot import CommentBot, actions
from .bot import CommentBot, PullRequestWorkflowBot, actions

event_payload = json.loads(event_payload.read())

bot = CommentBot(name='github-actions', handler=actions, token=arrow_token)
bot.handle(event_name, event_payload)
if 'comment' in event_name:
bot = CommentBot(name='github-actions', handler=actions, token=arrow_token)
bot.handle(event_name, event_payload)
else:
bot = PullRequestWorkflowBot(event_name, event_payload, token=arrow_token)
bot.handle()


@archery.group("linking")
Expand Down
80 changes: 80 additions & 0 deletions dev/archery/archery/tests/fixtures/commit-search.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
{
"total_count": 1,
"incomplete_results": false,
"items": [
{
"url": "https://api.github.com/repos/ursa-labs/ursabot/issues/26",
"repository_url": "https://api.github.com/repos/ursa-labs/ursabot",
"labels_url": "https://api.github.com/repos/ursa-labs/ursabot/issues/26/labels{/name}",
"comments_url": "https://api.github.com/repos/ursa-labs/ursabot/issues/26/comments",
"events_url": "https://api.github.com/repos/ursa-labs/ursabot/issues/26/events",
"html_url": "https://github.com/ursa-labs/ursabot/pull/26",
"id": 1572348024,
"node_id": "PR_kwDOHHgT285JU-KQ",
"number": 26,
"title": "Pr workflow automation poc",
"user": {
"login": "raulcd",
"id": 639755,
"node_id": "MDQ6VXNlcjYzOTc1NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/639755?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/raulcd",
"html_url": "https://github.com/raulcd",
"followers_url": "https://api.github.com/users/raulcd/followers",
"following_url": "https://api.github.com/users/raulcd/following{/other_user}",
"gists_url": "https://api.github.com/users/raulcd/gists{/gist_id}",
"starred_url": "https://api.github.com/users/raulcd/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/raulcd/subscriptions",
"organizations_url": "https://api.github.com/users/raulcd/orgs",
"repos_url": "https://api.github.com/users/raulcd/repos",
"events_url": "https://api.github.com/users/raulcd/events{/privacy}",
"received_events_url": "https://api.github.com/users/raulcd/received_events",
"type": "User",
"site_admin": false
},
"labels": [

],
"state": "open",
"locked": false,
"assignee": null,
"assignees": [

],
"milestone": null,
"comments": 1,
"created_at": "2023-02-06T10:56:32Z",
"updated_at": "2023-02-10T11:30:31Z",
"closed_at": null,
"author_association": "OWNER",
"active_lock_reason": null,
"draft": true,
"pull_request": {
"url": "https://api.github.com/repos/ursa-labs/ursabot/pulls/26",
"html_url": "https://github.com/ursa-labs/ursabot/pull/26",
"diff_url": "https://github.com/ursa-labs/ursabot/pull/26.diff",
"patch_url": "https://github.com/ursa-labs/ursabot/pull/26.patch",
"merged_at": null
},
"body": "<!--\r\nThanks for opening a pull request!\r\nIf this is your first pull request you can find detailed information on how \r\nto contribute here:\r\n * [New Contributor's Guide](https://arrow.apache.org/docs/dev/developers/guide/step_by_step/pr_lifecycle.html#reviews-and-merge-of-the-pull-request)\r\n * [Contributing Overview](https://arrow.apache.org/docs/dev/developers/overview.html)\r\n\r\n\r\nIf this is not a [minor PR](https://github.com/apache/arrow/blob/master/CONTRIBUTING.md#Minor-Fixes). Could you open an issue for this pull request on GitHub? https://github.com/apache/arrow/issues/new/choose\r\n\r\nOpening GitHub issues ahead of time contributes to the [Openness](http://theapacheway.com/open/#:~:text=Openness%20allows%20new%20users%20the,must%20happen%20in%20the%20open.) of the Apache Arrow project.\r\n\r\nThen could you also rename the pull request title in the following format?\r\n\r\n GH-${GITHUB_ISSUE_ID}: [${COMPONENT}] ${SUMMARY}\r\n\r\nor\r\n\r\n MINOR: [${COMPONENT}] ${SUMMARY}\r\n\r\nIn the case of PARQUET issues on JIRA the title also supports:\r\n\r\n PARQUET-${JIRA_ISSUE_ID}: [${COMPONENT}] ${SUMMARY}\r\n\r\n-->\r\n\r\n### Rationale for this change\r\n\r\n<!--\r\n Why are you proposing this change? If this is already explained clearly in the issue then this section is not needed.\r\n Explaining clearly why changes are proposed helps reviewers understand your changes and offer better suggestions for fixes. \r\n-->\r\n\r\n### What changes are included in this PR?\r\n\r\n<!--\r\nThere is no need to duplicate the description in the issue here but it is sometimes worth providing a summary of the individual changes in this PR.\r\n-->\r\n\r\n### Are these changes tested?\r\n\r\n<!--\r\nWe typically require tests for all PRs in order to:\r\n1. Prevent the code from being accidentally broken by subsequent changes\r\n2. Serve as another way to document the expected behavior of the code\r\n\r\nIf tests are not included in your PR, please explain why (for example, are they covered by existing tests)?\r\n-->\r\n\r\n### Are there any user-facing changes?\r\n\r\n<!--\r\nIf there are user-facing changes then we may require documentation to be updated before approving the PR.\r\n-->\r\n\r\n<!--\r\nIf there are any breaking changes to public APIs, please uncomment the line below and explain which changes are breaking.\r\n-->\r\n<!-- **This PR includes breaking changes to public APIs.** -->\r\n\r\n<!--\r\nPlease uncomment the line below (and provide explanation) if the changes fix either (a) a security vulnerability, (b) a bug that caused incorrect or invalid data to be produced, or (c) a bug that causes a crash (even when the API contract is upheld). We use this to highlight fixes to issues that may affect users without their knowledge. For this reason, fixing bugs that cause errors don't count, since those are usually obvious.\r\n-->\r\n<!-- **This PR contains a \"Critical Fix\".** -->",
"reactions": {
"url": "https://api.github.com/repos/ursa-labs/ursabot/issues/26/reactions",
"total_count": 0,
"+1": 0,
"-1": 0,
"laugh": 0,
"hooray": 0,
"confused": 0,
"heart": 0,
"rocket": 0,
"eyes": 0
},
"timeline_url": "https://api.github.com/repos/ursa-labs/ursabot/issues/26/timeline",
"performed_via_github_app": null,
"state_reason": null,
"score": 1.0
}
]
}

Loading