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 all 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
97 changes: 97 additions & 0 deletions .github/workflows/pr_bot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# 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: "Workflow label bot"
on:
pull_request_target:
types:
- opened
- converted_to_draft
- ready_for_review
- synchronize
workflow_run:
workflows: ["Label when reviewed"]
types: ['completed']

permissions:
contents: read
pull-requests: write
issues: write

raulcd marked this conversation as resolved.
Show resolved Hide resolved
jobs:
pr-workflow-bot-job:
name: "PR Workflow bot"
runs-on: ubuntu-latest
steps:
- name: 'Download PR review payload'
id: 'download'
if: github.event_name == 'workflow_run'
uses: actions/github-script@v6
with:
script: |
const run_id = "${{ github.event.workflow_run.id }}";
let artifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: run_id,
});
let pr_review_artifact = artifacts.data.artifacts.filter((artifact) => {
return artifact.name == "pr_review_payload"
})[0];
let pr_review_download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: pr_review_artifact.id,
archive_format: 'zip',
});
var fs = require('fs');
fs.writeFileSync('${{github.workspace}}/pr_review.zip', Buffer.from(pr_review_download.data));
- name: Extract artifact
id: extract
if: github.event_name == 'workflow_run'
run: |
unzip pr_review.zip
echo "pr_review_path=$(pwd)/event.json" >> $GITHUB_OUTPUT
- name: Checkout Arrow
uses: actions/checkout@v3
with:
path: arrow
raulcd marked this conversation as resolved.
Show resolved Hide resolved
repository: apache/arrow
ref: main
persist-credentials: false
# 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 }}
run: |
if [ "${GITHUB_EVENT_NAME}" = "workflow_run" ]; then
# workflow_run is executed on PR review. Update to original event.
archery trigger-bot \
--event-name "pull_request_review" \
--event-payload "${{ steps.extract.outputs.pr_review_path }}"
else
archery trigger-bot \
--event-name "${{ github.event_name }}" \
--event-payload "${{ github.event_path }}"
fi
32 changes: 32 additions & 0 deletions .github/workflows/pr_review_trigger.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# 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: "Label when reviewed"
on: pull_request_review

jobs:
# due to GitHub Actions permissions we can't change labels on the pull_request_review
# workflow. We trigger a new workflow run which will have permissions to add labels.
label-when-reviewed:
name: "Label PRs when reviewed"
runs-on: ubuntu-latest
steps:
- name: "Upload PR review Payload"
uses: actions/upload-artifact@v3
with:
path: "${{ github.event_path }}"
name: "pr_review_payload"
123 changes: 122 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,122 @@ 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.
"""
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()
next_state = self.compute_next_state(current_state)
if not current_state or current_state != next_state:
if current_state:
self.clear_current_state()
self.set_state(next_state)

def get_current_state(self):
"""
Returns a PullRequestState with the current PR state label
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 PullRequestState(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 compute_next_state(self, current_state):
"""
Returns the expected next state based on the event and
the current state.
"""
if (self.event_name == "pull_request_target" 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, requested changes or approved
if current_state in (
PullRequestState.change_review,
PullRequestState.changes,
PullRequestState.merge):
return current_state
else:
return PullRequestState.committer_review
if review_state == 'approved':
return PullRequestState.merge
else:
return PullRequestState.changes
elif (self.event_name == "pull_request_target" and
self.event_payload['action'] == 'synchronize' and
current_state == PullRequestState.changes):
return PullRequestState.change_review
# Default already opened PRs to Review state.
if current_state is None:
current_state = PullRequestState.review
return current_state

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


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
Loading