Skip to content

Commit

Permalink
[ci][tvmbot] Enable re-run for GitHub Actions (#12295)
Browse files Browse the repository at this point in the history
This adds the right permissions so anyone associated with the repo can trigger a re-run (GitHub hasn't flagged all committers as repo `COLLABORATORS` for some reason so it's difficult to determine from a username who has commit rights) and makes it so `@tvm-bot rerun` also re-runs all the Actions on a PR.
  • Loading branch information
driazati authored Aug 4, 2022
1 parent 3731a8c commit 78b3fc2
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 33 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/tvmbot.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@

name: tvm-bot
on:
status:
pull_request_review:
types:
- submitted
Expand All @@ -28,6 +27,7 @@ jobs:
- name: Run tvm-bot
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_ACTIONS_TOKEN: ${{ secrets.GH_ACTIONS_TOKEN }}
TVM_BOT_JENKINS_TOKEN: ${{ secrets.TVM_BOT_JENKINS_TOKEN }}
PR_NUMBER: ${{ github.event.issue.number }}
ISSUE_COMMENT: ${{ toJson(github.event.comment) }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,15 +81,15 @@
"invalid-author": {
"number": 10786,
"filename": "pr10786-invalid-author.json",
"expected": "Comment is not from from PR author or collaborator, quitting",
"expected": "Failed auth check 'collaborators', quitting",
"comment": "@tvm-bot merge",
"user": "not-abc",
"detail": "Merge requester is not a committer and cannot merge",
},
"unauthorized-comment": {
"number": 11244,
"filename": "pr11244-unauthorized-comment.json",
"expected": "Comment is not from from PR author or collaborator, quitting",
"expected": "Failed auth check 'collaborators'",
"comment": "@tvm-bot merge",
"user": "not-abc2",
"detail": "Check that a merge comment not from a CONTRIBUTOR is rejected",
Expand Down Expand Up @@ -135,7 +135,7 @@
[tuple(d.values()) for d in TEST_DATA.values()],
ids=TEST_DATA.keys(),
)
def test_mergebot(tmpdir_factory, number, filename, expected, comment, user, detail):
def test_tvmbot(tmpdir_factory, number, filename, expected, comment, user, detail):
"""
Test the mergebot test cases
"""
Expand All @@ -156,7 +156,7 @@ def test_mergebot(tmpdir_factory, number, filename, expected, comment, user, det
"login": user,
},
}
collaborators = []
collaborators = ["abc"]

proc = subprocess.run(
[
Expand All @@ -170,6 +170,8 @@ def test_mergebot(tmpdir_factory, number, filename, expected, comment, user, det
json.dumps(test_data),
"--testing-collaborators-json",
json.dumps(collaborators),
"--testing-mentionable-users-json",
json.dumps(collaborators),
"--trigger-comment-json",
json.dumps(comment),
],
Expand All @@ -178,6 +180,7 @@ def test_mergebot(tmpdir_factory, number, filename, expected, comment, user, det
encoding="utf-8",
env={
"TVM_BOT_JENKINS_TOKEN": "123",
"GH_ACTIONS_TOKEN": "123",
},
cwd=git.cwd,
check=False,
Expand Down
6 changes: 3 additions & 3 deletions tests/scripts/git_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,9 @@ def _request(self, full_url: str, body: Dict[str, Any], method: str) -> Dict[str
with request.urlopen(req, data) as response:
content = response.read()
except error.HTTPError as e:
logging.info(f"Error response: {e.read().decode()}")
e.seek(0)
raise e
msg = str(e)
error_data = e.read().decode()
raise RuntimeError(f"Error response: {msg}\n{error_data}")

logging.info(f"Got response from {full_url}: {content}")
try:
Expand Down
137 changes: 112 additions & 25 deletions tests/scripts/github_tvmbot.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@

EXPECTED_JOBS = ["tvm-ci/pr-head"]
TVM_BOT_JENKINS_TOKEN = os.environ["TVM_BOT_JENKINS_TOKEN"]
GH_ACTIONS_TOKEN = os.environ["GH_ACTIONS_TOKEN"]
JENKINS_URL = "https://ci.tlcpack.ai/"
THANKS_MESSAGE = r"(\s*)Thanks for contributing to TVM! Please refer to guideline https://tvm.apache.org/docs/contribute/ for useful information and tips. After the pull request is submitted, please request code reviews from \[Reviewers\]\(https://github.com/apache/incubator-tvm/blob/master/CONTRIBUTORS.md#reviewers\) by them in the pull request thread.(\s*)"

Expand All @@ -57,6 +58,18 @@ def to_json_str(obj: Any) -> str:
}
"""

MENTIONABLE_QUERY = """
query ($owner: String!, $name: String!, $user: String!) {
repository(owner: $owner, name: $name) {
mentionableUsers(query: $user, first: 1) {
nodes {
login
}
}
}
}
"""


PR_QUERY = """
query ($owner: String!, $name: String!, $number: Int!) {
Expand Down Expand Up @@ -106,6 +119,7 @@ def to_json_str(obj: Any) -> str:
nodes {
... on CheckRun {
name
databaseId
checkSuite {
workflowRun {
workflow {
Expand Down Expand Up @@ -221,12 +235,12 @@ def checker(obj, parent_key):
def __repr__(self):
return json.dumps(self.raw, indent=2)

def plus_one(self, comment: Dict[str, Any]):
def react(self, comment: Dict[str, Any], content: str):
"""
React with a thumbs up to a comment
"""
url = f"issues/comments/{comment['id']}/reactions"
data = {"content": "+1"}
data = {"content": content}
if self.dry_run:
logging.info(f"Dry run, would have +1'ed to {url} with {data}")
else:
Expand Down Expand Up @@ -326,14 +340,20 @@ def search_collaborator(self, user: str) -> List[Dict[str, Any]]:
"""
Query GitHub for collaborators matching 'user'
"""
return self.search_users(user, COLLABORATORS_QUERY)["collaborators"]["nodes"]

def search_users(self, user: str, query: str) -> List[Dict[str, Any]]:
return self.github.graphql(
query=COLLABORATORS_QUERY,
query=query,
variables={
"owner": self.owner,
"name": self.repo_name,
"user": user,
},
)["data"]["repository"]["collaborators"]["nodes"]
)["data"]["repository"]

def search_mentionable_users(self, user: str) -> List[Dict[str, Any]]:
return self.search_users(user, MENTIONABLE_QUERY)["mentionableUsers"]["nodes"]

def comment(self, text: str) -> None:
"""
Expand Down Expand Up @@ -503,6 +523,30 @@ def rerun_jenkins_ci(self) -> None:
else:
post(url, auth=("tvm-bot", TVM_BOT_JENKINS_TOKEN))

def rerun_github_actions(self) -> None:
job_ids = []
for item in self.head_commit()["statusCheckRollup"]["contexts"]["nodes"]:
if "checkSuite" in item:
job_ids.append(item["databaseId"])

logging.info(f"Rerunning GitHub Actions jobs with IDs: {job_ids}")
actions_github = GitHubRepo(
user=self.github.user, repo=self.github.repo, token=GH_ACTIONS_TOKEN
)
for job_id in job_ids:
if self.dry_run:
try:
actions_github.post(f"actions/jobs/{job_id}/rerun", data={})
except RuntimeError as e:
# Ignore errors about jobs that are part of the same workflow to avoid
# having to figure out which jobs are in which workflows ahead of time
if "The workflow run containing this job is already running" in str(e):
pass
else:
raise e
else:
logging.info(f"Dry run, not restarting {job_id}")

def comment_failure(self, msg: str, exception: Exception):
if not self.dry_run:
exception_msg = traceback.format_exc()
Expand All @@ -514,13 +558,58 @@ def comment_failure(self, msg: str, exception: Exception):
return exception


def check_author(pr, triggering_comment, args):
comment_author = triggering_comment["user"]["login"]
if pr.author() == comment_author:
logging.info("Comment user is PR author, continuing")
return True
return False


def check_collaborator(pr, triggering_comment, args):
logging.info("Checking collaborators")
# Get the list of collaborators for the repo filtered by the comment
# author
commment_author = triggering_comment["user"]["login"]
if args.testing_collaborators_json:
collaborators = json.loads(args.testing_collaborators_json)
else:
collaborators = pr.search_collaborator(commment_author)
logging.info(f"Found collaborators: {collaborators}")

return len(collaborators) > 0 and commment_author in collaborators


def check_mentionable_users(pr, triggering_comment, args):
logging.info("Checking mentionable users")
commment_author = triggering_comment["user"]["login"]
if args.testing_mentionable_users_json:
mentionable_users = json.loads(args.testing_mentionable_users_json)
else:
mentionable_users = pr.search_mentionable_users(commment_author)
logging.info(f"Found mentionable_users: {mentionable_users}")

return len(mentionable_users) > 0 and commment_author in mentionable_users


AUTH_CHECKS = {
"metionable_users": check_mentionable_users,
"collaborators": check_collaborator,
"author": check_author,
}
# Stash the keys so they're accessible from the values
AUTH_CHECKS = {k: (k, v) for k, v in AUTH_CHECKS.items()}


class Merge:
triggers = [
"merge",
"merge this",
"merge this pr",
]

auth = [AUTH_CHECKS["collaborators"], AUTH_CHECKS["author"]]

@staticmethod
def run(pr: PR):
info = None
Expand Down Expand Up @@ -548,9 +637,15 @@ class Rerun:
"run ci",
]

auth = [AUTH_CHECKS["metionable_users"]]

@staticmethod
def run(pr: PR):
pr.rerun_jenkins_ci()
try:
pr.rerun_jenkins_ci()
pr.rerun_github_actions()
except Exception as e:
pr.comment_failure("Failed to re-run CI", e)


if __name__ == "__main__":
Expand All @@ -566,6 +661,9 @@ def run(pr: PR):
parser.add_argument(
"--testing-collaborators-json", help="(testing only) manual data for testing"
)
parser.add_argument(
"--testing-mentionable-users-json", help="(testing only) manual data for testing"
)
parser.add_argument(
"--dry-run",
action="store_true",
Expand Down Expand Up @@ -615,29 +713,18 @@ def run(pr: PR):
else:
pr = PR(number=int(args.pr), owner=owner, repo=repo, dry_run=args.dry_run)

# Acknowledge the comment with a react
pr.plus_one(comment)

# Check the comment author
comment_author = comment["user"]["login"]
if pr.author() == comment_author:
logging.info("Comment user is PR author, continuing")
else:
logging.info("Comment is not from PR author, checking collaborators")
# Get the list of collaborators for the repo filtered by the comment
# author
if args.testing_collaborators_json:
collaborators = json.loads(args.testing_collaborators_json)
else:
collaborators = pr.search_collaborator(comment_author)
logging.info(f"Found collaborators: {collaborators}")

if len(collaborators) > 0:
logging.info("Comment is from collaborator")
for name, check in command_to_run.auth:
if check(pr, comment, args):
logging.info(f"Passed auth check '{name}', continuing")
else:
logging.info("Comment is not from from PR author or collaborator, quitting")
logging.info(f"Failed auth check '{name}', quitting")
# Add a sad face
pr.react(comment, "confused")
exit(0)

# Acknowledge the comment with a react
pr.react(comment, "+1")

state = pr.state()

if state != "OPEN":
Expand Down

0 comments on commit 78b3fc2

Please sign in to comment.