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

[ci][tvmbot] Enable re-run for GitHub Actions #12295

Merged
merged 5 commits into from
Aug 4, 2022
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
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):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this imply if one of the actions is still running from the last post, it will continue to run and the new instance of the same action will not start on a @tvm-bot rerun?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, a point for potential future improvement could also be to only restart jobs that failed on a rerun. Since the commit hasn't changed the other actions that have passed likely do not need to be rerun.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think (like 70% certain) this only comes up for double-requesting a re-run of the same job, so it should still restart everything from the previous set of jobs. Restarting only failed jobs is a good idea, I'll leave it for a follow up PR though

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