You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tvm.apache.org by dr...@apache.org on 2022/09/02 16:44:32 UTC

[tvm] branch main updated: [ci][tvmbot] Trigger GitHub Actions after merging (#12361)

This is an automated email from the ASF dual-hosted git repository.

driazati pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tvm.git


The following commit(s) were added to refs/heads/main by this push:
     new 7c7b0f7a2f [ci][tvmbot] Trigger GitHub Actions after merging (#12361)
7c7b0f7a2f is described below

commit 7c7b0f7a2fb7833a3afe8900f8b38ccf144f96f0
Author: driazati <94...@users.noreply.github.com>
AuthorDate: Fri Sep 2 09:44:22 2022 -0700

    [ci][tvmbot] Trigger GitHub Actions after merging (#12361)
    
    This fixes the issue where merging from GitHub Actions (i.e. with the default `GITHUB_TOKEN`) doesn't trigger post merge GitHub Actions on the commit it creates in `main`. Instead these jobs are triggered manually by a call to the Actions API after the merge has taken place.
    
    This also updates the tvmbot testing code (and by extension some of the other CI testing code) to remove the fixtures for each test in favor of constructing them from a single sample at runtime, this makes it a lot easier to add new tests and see what is different between each data sample and clean up the testing anti-patterns that were there before (e.g. `run()` instead of `pytest.mark.parameterize`, but none of the tests in `test_ci.py` have changed)
    
    Tested in https://github.com/driazati/tvm/pull/36 which ran https://github.com/driazati/tvm/actions/runs/2881047903
---
 ci/scripts/github_tvmbot.py                        |  22 +-
 tests/python/ci/sample_prs/pr10786-badci.json      | 130 ----
 .../ci/sample_prs/pr10786-changes-requested.json   | 131 ----
 tests/python/ci/sample_prs/pr10786-co-authors.json | 129 ----
 .../ci/sample_prs/pr10786-invalid-author.json      | 130 ----
 tests/python/ci/sample_prs/pr10786-merges.json     | 129 ----
 .../python/ci/sample_prs/pr10786-missing-job.json  | 129 ----
 .../python/ci/sample_prs/pr10786-nottriggered.json | 129 ----
 tests/python/ci/sample_prs/pr10786-oldreview.json  | 129 ----
 .../{pr10786-ignore-jobs.json => pr10786.json}     |   5 +-
 .../sample_prs/pr11244-unauthorized-comment.json   | 103 ---
 tests/python/ci/sample_prs/pr11267-no-review.json  | 144 ----
 tests/python/ci/sample_prs/pr11442-rerun-ci.json   | 183 -----
 tests/python/ci/test_ci.py                         | 803 +++++++++------------
 tests/python/ci/test_tvmbot.py                     | 400 +++++-----
 tests/python/ci/test_utils.py                      |  33 +-
 16 files changed, 624 insertions(+), 2105 deletions(-)

diff --git a/ci/scripts/github_tvmbot.py b/ci/scripts/github_tvmbot.py
index 3a39e69694..ee9607dd02 100755
--- a/ci/scripts/github_tvmbot.py
+++ b/ci/scripts/github_tvmbot.py
@@ -195,6 +195,7 @@ class PR:
         self.number = number
         self.repo_name = repo
         self.dry_run = dry_run
+        self.has_error = False
 
         if dry_run and raw_data:
             # In test mode there is no need to fetch anything
@@ -468,7 +469,10 @@ class PR:
 
     def trigger_gha_ci(self, sha: str) -> None:
         logging.info(f"POST-ing a workflow_dispatch event to main.yml")
-        r = self.github.post(
+        actions_github = GitHubRepo(
+            user=self.github.user, repo=self.github.repo, token=GH_ACTIONS_TOKEN
+        )
+        r = actions_github.post(
             url="actions/workflows/main.yml/dispatches",
             data={
                 "ref": "main",
@@ -537,9 +541,12 @@ class PR:
 
         workflow_ids = list(set(workflow_ids))
         logging.info(f"Rerunning GitHub Actions workflows with IDs: {workflow_ids}")
-        actions_github = GitHubRepo(
-            user=self.github.user, repo=self.github.repo, token=GH_ACTIONS_TOKEN
-        )
+        if self.dry_run:
+            actions_github = None
+        else:
+            actions_github = GitHubRepo(
+                user=self.github.user, repo=self.github.repo, token=GH_ACTIONS_TOKEN
+            )
         for workflow_id in workflow_ids:
             if self.dry_run:
                 logging.info(f"Dry run, not restarting workflow {workflow_id}")
@@ -576,6 +583,7 @@ class PR:
             comment += "</details>"
 
         pr.comment(comment)
+        pr.has_error = True
         return exception
 
 
@@ -750,6 +758,9 @@ if __name__ == "__main__":
     for name, check in command_to_run.auth:
         if check(pr, comment, args):
             logging.info(f"Passed auth check '{name}', continuing")
+            # Only one authorization check needs to pass (e.g. just mentionable
+            # or PR author), not all of them so quit
+            break
         else:
             logging.info(f"Failed auth check '{name}', quitting")
             # Add a sad face
@@ -767,3 +778,6 @@ if __name__ == "__main__":
 
     # Run the command
     command_to_run.run(pr)
+
+    if pr.has_error:
+        raise RuntimeError("PR commented a failure")
diff --git a/tests/python/ci/sample_prs/pr10786-badci.json b/tests/python/ci/sample_prs/pr10786-badci.json
deleted file mode 100644
index 7e9d10d0b6..0000000000
--- a/tests/python/ci/sample_prs/pr10786-badci.json
+++ /dev/null
@@ -1,130 +0,0 @@
-{
-  "title": "[Hexagon] 2-d allocation cleanup",
-  "body": "- Added device validity check in allocation. HexagonDeviceAPI should only be called for CPU/Hexagon types.\r\n\r\n- Check for \"global.vtcm\" scope instead of \"vtcm\".  The ccope of N-d allocations produced by `LowerVtcmAlloc` should be `\"global.vtcm\"`.  The previous check allowed unsupported scope such as `\"local.vtcm\"`.\r\n\r\n- Remove `vtcmallocs` entry after calling free. Previously, the vtcm allocation map kept dangling pointers to `HexagonBuffer` objects after they  [...]
-  "state": "OPEN",
-  "author": {
-    "login": "abc"
-  },
-  "comments": {
-    "pageInfo": {
-      "hasPreviousPage": false
-    },
-    "nodes": []
-  },
-  "authorCommits": {
-    "nodes": [
-      {
-        "commit": {
-          "authors": {
-            "nodes": [
-              {
-                "name": "Eric Lunderberg",
-                "email": "elunderberg@octoml.ai"
-              },
-              {
-                "name": "Adam Straw",
-                "email": "astraw@octoml.ai"
-              }
-            ]
-          }
-        }
-      }
-    ]
-  },
-  "commits": {
-    "nodes": [
-      {
-        "commit": {
-          "oid": "6f04bcf57d07f915a98fd91178f04d9e92a09fcd",
-          "statusCheckRollup": {
-            "contexts": {
-              "pageInfo": {
-                "hasNextPage": false
-              },
-              "nodes": [
-                {
-                  "name": "MacOS",
-                  "checkSuite": {
-                    "workflowRun": {
-                      "workflow": {
-                        "name": "CI"
-                      }
-                    }
-                  },
-                  "status": "COMPLETED",
-                  "conclusion": "SUCCESS",
-                  "url": "https://github.com/apache/tvm/runs/5694945392"
-                },
-                {
-                  "name": "cc-reviewers",
-                  "checkSuite": {
-                    "workflowRun": {
-                      "workflow": {
-                        "name": "PR"
-                      }
-                    }
-                  },
-                  "status": "COMPLETED",
-                  "conclusion": "SUCCESS",
-                  "url": "https://github.com/apache/tvm/runs/5694945029"
-                },
-                {
-                  "name": "tag-teams",
-                  "checkSuite": {
-                    "workflowRun": {
-                      "workflow": {
-                        "name": "Teams"
-                      }
-                    }
-                  },
-                  "status": "COMPLETED",
-                  "conclusion": "SUCCESS",
-                  "url": "https://github.com/apache/tvm/runs/5694945030"
-                },
-                {
-                  "name": "Windows",
-                  "checkSuite": {
-                    "workflowRun": {
-                      "workflow": {
-                        "name": "CI"
-                      }
-                    }
-                  },
-                  "status": "COMPLETED",
-                  "conclusion": "SUCCESS",
-                  "url": "https://github.com/apache/tvm/runs/5694945524"
-                },
-                {
-                  "state": "FAILED",
-                  "context": "tvm-ci/pr-head",
-                  "targetUrl": "https://ci.tlcpack.ai/job/tvm/job/PR-10786/1/display/redirect"
-                }
-              ]
-            }
-          }
-        }
-      }
-    ]
-  },
-  "reviewDecision": "APPROVED",
-  "reviews": {
-    "pageInfo": {
-      "hasPreviousPage": false
-    },
-    "nodes": [
-      {
-        "body": "@tvm-bot merge",
-        "updatedAt": "2022-03-25T22:13:50Z",
-        "authorCanPushToRepository": true,
-        "commit": {
-          "oid": "6f04bcf57d07f915a98fd91178f04d9e92a09fcd"
-        },
-        "id": 123,
-        "author": {
-          "login": "kparzysz-quic"
-        },
-        "state": "APPROVED"
-      }
-    ]
-  }
-}
\ No newline at end of file
diff --git a/tests/python/ci/sample_prs/pr10786-changes-requested.json b/tests/python/ci/sample_prs/pr10786-changes-requested.json
deleted file mode 100644
index 24e261099a..0000000000
--- a/tests/python/ci/sample_prs/pr10786-changes-requested.json
+++ /dev/null
@@ -1,131 +0,0 @@
-{
-  "title": "[Hexagon] 2-d allocation cleanup",
-  "body": "- Added device validity check in allocation. HexagonDeviceAPI should only be called for CPU/Hexagon types.\r\n\r\n- Check for \"global.vtcm\" scope instead of \"vtcm\".  The ccope of N-d allocations produced by `LowerVtcmAlloc` should be `\"global.vtcm\"`.  The previous check allowed unsupported scope such as `\"local.vtcm\"`.\r\n\r\n- Remove `vtcmallocs` entry after calling free. Previously, the vtcm allocation map kept dangling pointers to `HexagonBuffer` objects after they  [...]
-  "state": "OPEN",
-  "author": {
-    "login": "abc"
-  },
-  "comments": {
-    "pageInfo": {
-      "hasPreviousPage": false
-    },
-    "nodes": []
-  },
-  "authorCommits": {
-    "nodes": [
-      {
-        "commit": {
-          "authors": {
-            "nodes": [
-              {
-                "name": "Eric Lunderberg",
-                "email": "elunderberg@octoml.ai"
-              },
-              {
-                "name": "Adam Straw",
-                "email": "astraw@octoml.ai"
-              }
-            ]
-          }
-        }
-      }
-    ]
-  },
-  "commits": {
-    "nodes": [
-      {
-        "commit": {
-          "oid": "6f04bcf57d07f915a98fd91178f04d9e92a09fcd",
-          "statusCheckRollup": {
-            "contexts": {
-              "pageInfo": {
-                "hasNextPage": false
-              },
-              "nodes": [
-                {
-                  "name": "MacOS",
-                  "checkSuite": {
-                    "workflowRun": {
-                      "workflow": {
-                        "name": "CI"
-                      }
-                    }
-                  },
-                  "status": "COMPLETED",
-                  "conclusion": "SUCCESS",
-                  "url": "https://github.com/apache/tvm/runs/5694945392"
-                },
-                {
-                  "name": "cc-reviewers",
-                  "checkSuite": {
-                    "workflowRun": {
-                      "workflow": {
-                        "name": "PR"
-                      }
-                    }
-                  },
-                  "status": "COMPLETED",
-                  "conclusion": "SUCCESS",
-                  "url": "https://github.com/apache/tvm/runs/5694945029"
-                },
-                {
-                  "name": "tag-teams",
-                  "checkSuite": {
-                    "workflowRun": {
-                      "workflow": {
-                        "name": "Teams"
-                      }
-                    }
-                  },
-                  "status": "COMPLETED",
-                  "conclusion": "SUCCESS",
-                  "url": "https://github.com/apache/tvm/runs/5694945030"
-                },
-                {
-                  "name": "Windows",
-                  "checkSuite": {
-                    "workflowRun": {
-                      "workflow": {
-                        "name": "CI"
-                      }
-                    }
-                  },
-                  "status": "COMPLETED",
-                  "conclusion": "SUCCESS",
-                  "url": "https://github.com/apache/tvm/runs/5694945524"
-                },
-                {
-                  "state": "SUCCESS",
-                  "context": "tvm-ci/pr-head",
-                  "targetUrl": "https://ci.tlcpack.ai/job/tvm/job/PR-10786/1/display/redirect"
-                }
-              ]
-            }
-          }
-        }
-      }
-    ]
-  },
-  "reviewDecision": "CHANGES_REQUESTED",
-  "reviews": {
-    "pageInfo": {
-      "hasPreviousPage": false
-    },
-    "nodes": [
-      {
-        "body": "@tvm-bot merge",
-        "updatedAt": "2022-03-25T22:13:50Z",
-        "url": "https://github.com/apache/tvm/pull/10786#pullrequestreview-922186273",
-        "authorCanPushToRepository": true,
-        "commit": {
-          "oid": "6f04bcf57d07f915a98fd91178f04d9e92a09fcd"
-        },
-        "id": 123,
-        "author": {
-          "login": "kparzysz-quic"
-        },
-        "state": "CHANGES_REQUESTED"
-      }
-    ]
-  }
-}
\ No newline at end of file
diff --git a/tests/python/ci/sample_prs/pr10786-co-authors.json b/tests/python/ci/sample_prs/pr10786-co-authors.json
deleted file mode 100644
index 75f2728250..0000000000
--- a/tests/python/ci/sample_prs/pr10786-co-authors.json
+++ /dev/null
@@ -1,129 +0,0 @@
-{
-  "title": "[Hexagon] 2-d allocation cleanup",
-  "body": "- Added device validity check in allocation. HexagonDeviceAPI should only be called for CPU/Hexagon types.\r\n\r\n- Check for \"global.vtcm\" scope instead of \"vtcm\".  The ccope of N-d allocations produced by `LowerVtcmAlloc` should be `\"global.vtcm\"`.  The previous check allowed unsupported scope such as `\"local.vtcm\"`.\r\n\r\n- Remove `vtcmallocs` entry after calling free. Previously, the vtcm allocation map kept dangling pointers to `HexagonBuffer` objects after they  [...]
-  "state": "OPEN",
-  "author": {
-    "login": "abc"
-  },
-  "comments": {
-    "pageInfo": {
-      "hasPreviousPage": false
-    },
-    "nodes": []
-  },
-  "authorCommits": {
-    "nodes": [
-      {
-        "commit": {
-          "authors": {
-            "nodes": [
-              {
-                "name": "Eric Lunderberg",
-                "email": "elunderberg@octoml.ai"
-              },
-              {
-                "name": "Some One",
-                "email": "someone@email.com"
-              }
-            ]
-          }
-        }
-      }
-    ]
-  },
-  "commits": {
-    "nodes": [
-      {
-        "commit": {
-          "oid": "6f04bcf57d07f915a98fd91178f04d9e92a09fcd",
-          "statusCheckRollup": {
-            "contexts": {
-              "pageInfo": {
-                "hasNextPage": false
-              },
-              "nodes": [
-                {
-                  "name": "MacOS",
-                  "checkSuite": {
-                    "workflowRun": {
-                      "workflow": {
-                        "name": "CI"
-                      }
-                    }
-                  },
-                  "status": "COMPLETED",
-                  "conclusion": "SUCCESS",
-                  "url": "https://github.com/apache/tvm/runs/5694945392"
-                },
-                {
-                  "name": "cc-reviewers",
-                  "checkSuite": {
-                    "workflowRun": {
-                      "workflow": {
-                        "name": "PR"
-                      }
-                    }
-                  },
-                  "status": "COMPLETED",
-                  "conclusion": "SUCCESS",
-                  "url": "https://github.com/apache/tvm/runs/5694945029"
-                },
-                {
-                  "name": "tag-teams",
-                  "checkSuite": {
-                    "workflowRun": {
-                      "workflow": {
-                        "name": "Teams"
-                      }
-                    }
-                  },
-                  "status": "COMPLETED",
-                  "conclusion": "SUCCESS",
-                  "url": "https://github.com/apache/tvm/runs/5694945030"
-                },
-                {
-                  "name": "Windows",
-                  "checkSuite": {
-                    "workflowRun": {
-                      "workflow": {
-                        "name": "CI"
-                      }
-                    }
-                  },
-                  "status": "COMPLETED",
-                  "conclusion": "SUCCESS",
-                  "url": "https://github.com/apache/tvm/runs/5694945524"
-                },
-                {
-                  "state": "SUCCESS",
-                  "context": "tvm-ci/pr-head",
-                  "targetUrl": "https://ci.tlcpack.ai/job/tvm/job/PR-10786/1/display/redirect"
-                }
-              ]
-            }
-          }
-        }
-      }
-    ]
-  },
-  "reviewDecision": "APPROVED",
-  "reviews": {
-    "pageInfo": {
-      "hasPreviousPage": false
-    },
-    "nodes": [
-      {
-        "body": "@tvm-bot merge",
-        "updatedAt": "2022-03-25T22:13:50Z",
-        "authorCanPushToRepository": true,
-        "commit": {
-          "oid": "6f04bcf57d07f915a98fd91178f04d9e92a09fcd"
-        },
-        "author": {
-          "login": "kparzysz-quic"
-        },
-        "state": "APPROVED"
-      }
-    ]
-  }
-}
\ No newline at end of file
diff --git a/tests/python/ci/sample_prs/pr10786-invalid-author.json b/tests/python/ci/sample_prs/pr10786-invalid-author.json
deleted file mode 100644
index 81b028e319..0000000000
--- a/tests/python/ci/sample_prs/pr10786-invalid-author.json
+++ /dev/null
@@ -1,130 +0,0 @@
-{
-  "title": "[Hexagon] 2-d allocation cleanup",
-  "body": "- Added device validity check in allocation. HexagonDeviceAPI should only be called for CPU/Hexagon types.\r\n\r\n- Check for \"global.vtcm\" scope instead of \"vtcm\".  The ccope of N-d allocations produced by `LowerVtcmAlloc` should be `\"global.vtcm\"`.  The previous check allowed unsupported scope such as `\"local.vtcm\"`.\r\n\r\n- Remove `vtcmallocs` entry after calling free. Previously, the vtcm allocation map kept dangling pointers to `HexagonBuffer` objects after they  [...]
-  "state": "OPEN",
-  "author": {
-    "login": "abc"
-  },
-  "comments": {
-    "pageInfo": {
-      "hasPreviousPage": false
-    },
-    "nodes": []
-  },
-  "authorCommits": {
-    "nodes": [
-      {
-        "commit": {
-          "authors": {
-            "nodes": [
-              {
-                "name": "Eric Lunderberg",
-                "email": "elunderberg@octoml.ai"
-              },
-              {
-                "name": "Adam Straw",
-                "email": "astraw@octoml.ai"
-              }
-            ]
-          }
-        }
-      }
-    ]
-  },
-  "commits": {
-    "nodes": [
-      {
-        "commit": {
-          "oid": "6f04bcf57d07f915a98fd91178f04d9e92a09fcd",
-          "statusCheckRollup": {
-            "contexts": {
-              "pageInfo": {
-                "hasNextPage": false
-              },
-              "nodes": [
-                {
-                  "name": "MacOS",
-                  "checkSuite": {
-                    "workflowRun": {
-                      "workflow": {
-                        "name": "CI"
-                      }
-                    }
-                  },
-                  "status": "COMPLETED",
-                  "conclusion": "SUCCESS",
-                  "url": "https://github.com/apache/tvm/runs/5694945392"
-                },
-                {
-                  "name": "cc-reviewers",
-                  "checkSuite": {
-                    "workflowRun": {
-                      "workflow": {
-                        "name": "PR"
-                      }
-                    }
-                  },
-                  "status": "COMPLETED",
-                  "conclusion": "SUCCESS",
-                  "url": "https://github.com/apache/tvm/runs/5694945029"
-                },
-                {
-                  "name": "tag-teams",
-                  "checkSuite": {
-                    "workflowRun": {
-                      "workflow": {
-                        "name": "Teams"
-                      }
-                    }
-                  },
-                  "status": "COMPLETED",
-                  "conclusion": "SUCCESS",
-                  "url": "https://github.com/apache/tvm/runs/5694945030"
-                },
-                {
-                  "name": "Windows",
-                  "checkSuite": {
-                    "workflowRun": {
-                      "workflow": {
-                        "name": "CI"
-                      }
-                    }
-                  },
-                  "status": "COMPLETED",
-                  "conclusion": "SUCCESS",
-                  "url": "https://github.com/apache/tvm/runs/5694945524"
-                },
-                {
-                  "state": "SUCCESS",
-                  "context": "tvm-ci/pr-head",
-                  "targetUrl": "https://ci.tlcpack.ai/job/tvm/job/PR-10786/1/display/redirect"
-                }
-              ]
-            }
-          }
-        }
-      }
-    ]
-  },
-  "reviewDecision": "APPROVED",
-  "reviews": {
-    "pageInfo": {
-      "hasPreviousPage": false
-    },
-    "nodes": [
-      {
-        "body": "@tvm-bot merge",
-        "id": 123,
-        "updatedAt": "2022-03-25T22:13:50Z",
-        "authorCanPushToRepository": false,
-        "commit": {
-          "oid": "6f04bcf57d07f915a98fd91178f04d9e92a09fcd"
-        },
-        "author": {
-          "login": "kparzysz-quic"
-        },
-        "state": "APPROVED"
-      }
-    ]
-  }
-}
\ No newline at end of file
diff --git a/tests/python/ci/sample_prs/pr10786-merges.json b/tests/python/ci/sample_prs/pr10786-merges.json
deleted file mode 100644
index 0226c8ab52..0000000000
--- a/tests/python/ci/sample_prs/pr10786-merges.json
+++ /dev/null
@@ -1,129 +0,0 @@
-{
-  "title": "[Hexagon] 2-d allocation cleanup",
-  "body": "- Added device validity check in allocation. HexagonDeviceAPI should only be called for CPU/Hexagon types.\r\n\r\n- Check for \"global.vtcm\" scope instead of \"vtcm\".  The ccope of N-d allocations produced by `LowerVtcmAlloc` should be `\"global.vtcm\"`.  The previous check allowed unsupported scope such as `\"local.vtcm\"`.\r\n\r\n- Remove `vtcmallocs` entry after calling free.\n\n\nThanks for contributing to TVM!   Please refer to guideline https://tvm.apache.org/docs/cont [...]
-  "state": "OPEN",
-  "author": {
-    "login": "abc"
-  },
-  "comments": {
-    "pageInfo": {
-      "hasPreviousPage": false
-    },
-    "nodes": []
-  },
-  "authorCommits": {
-    "nodes": [
-      {
-        "commit": {
-          "authors": {
-            "nodes": [
-              {
-                "name": "Eric Lunderberg",
-                "email": "elunderberg@octoml.ai"
-              },
-              {
-                "name": "Adam Straw",
-                "email": "astraw@octoml.ai"
-              }
-            ]
-          }
-        }
-      }
-    ]
-  },
-  "commits": {
-    "nodes": [
-      {
-        "commit": {
-          "oid": "6f04bcf57d07f915a98fd91178f04d9e92a09fcd",
-          "statusCheckRollup": {
-            "contexts": {
-              "pageInfo": {
-                "hasNextPage": false
-              },
-              "nodes": [
-                {
-                  "name": "MacOS",
-                  "checkSuite": {
-                    "workflowRun": {
-                      "workflow": {
-                        "name": "CI"
-                      }
-                    }
-                  },
-                  "status": "COMPLETED",
-                  "conclusion": "SUCCESS",
-                  "url": "https://github.com/apache/tvm/runs/5694945392"
-                },
-                {
-                  "name": "cc-reviewers",
-                  "checkSuite": {
-                    "workflowRun": {
-                      "workflow": {
-                        "name": "PR"
-                      }
-                    }
-                  },
-                  "status": "COMPLETED",
-                  "conclusion": "SUCCESS",
-                  "url": "https://github.com/apache/tvm/runs/5694945029"
-                },
-                {
-                  "name": "tag-teams",
-                  "checkSuite": {
-                    "workflowRun": {
-                      "workflow": {
-                        "name": "Teams"
-                      }
-                    }
-                  },
-                  "status": "COMPLETED",
-                  "conclusion": "SUCCESS",
-                  "url": "https://github.com/apache/tvm/runs/5694945030"
-                },
-                {
-                  "name": "Windows",
-                  "checkSuite": {
-                    "workflowRun": {
-                      "workflow": {
-                        "name": "CI"
-                      }
-                    }
-                  },
-                  "status": "COMPLETED",
-                  "conclusion": "SUCCESS",
-                  "url": "https://github.com/apache/tvm/runs/5694945524"
-                },
-                {
-                  "state": "SUCCESS",
-                  "context": "tvm-ci/pr-head",
-                  "targetUrl": "https://ci.tlcpack.ai/job/tvm/job/PR-10786/1/display/redirect"
-                }
-              ]
-            }
-          }
-        }
-      }
-    ]
-  },
-  "reviewDecision": "APPROVED",
-  "reviews": {
-    "pageInfo": {
-      "hasPreviousPage": false
-    },
-    "nodes": [
-      {
-        "body": "@tvm-bot merge",
-        "updatedAt": "2022-03-25T22:13:50Z",
-        "authorCanPushToRepository": true,
-        "commit": {
-          "oid": "6f04bcf57d07f915a98fd91178f04d9e92a09fcd"
-        },
-        "author": {
-          "login": "kparzysz-quic"
-        },
-        "state": "APPROVED"
-      }
-    ]
-  }
-}
\ No newline at end of file
diff --git a/tests/python/ci/sample_prs/pr10786-missing-job.json b/tests/python/ci/sample_prs/pr10786-missing-job.json
deleted file mode 100644
index 13739b793f..0000000000
--- a/tests/python/ci/sample_prs/pr10786-missing-job.json
+++ /dev/null
@@ -1,129 +0,0 @@
-{
-  "title": "[Hexagon] 2-d allocation cleanup",
-  "body": "- Added device validity check in allocation. HexagonDeviceAPI should only be called for CPU/Hexagon types.\r\n\r\n- Check for \"global.vtcm\" scope instead of \"vtcm\".  The ccope of N-d allocations produced by `LowerVtcmAlloc` should be `\"global.vtcm\"`.  The previous check allowed unsupported scope such as `\"local.vtcm\"`.\r\n\r\n- Remove `vtcmallocs` entry after calling free. Previously, the vtcm allocation map kept dangling pointers to `HexagonBuffer` objects after they  [...]
-  "state": "OPEN",
-  "author": {
-    "login": "abc"
-  },
-  "comments": {
-    "pageInfo": {
-      "hasPreviousPage": false
-    },
-    "nodes": []
-  },
-  "authorCommits": {
-    "nodes": [
-      {
-        "commit": {
-          "authors": {
-            "nodes": [
-              {
-                "name": "Eric Lunderberg",
-                "email": "elunderberg@octoml.ai"
-              },
-              {
-                "name": "Adam Straw",
-                "email": "astraw@octoml.ai"
-              }
-            ]
-          }
-        }
-      }
-    ]
-  },
-  "commits": {
-    "nodes": [
-      {
-        "commit": {
-          "oid": "6f04bcf57d07f915a98fd91178f04d9e92a09fcd",
-          "statusCheckRollup": {
-            "contexts": {
-              "pageInfo": {
-                "hasNextPage": false
-              },
-              "nodes": [
-                {
-                  "name": "MacOS",
-                  "checkSuite": {
-                    "workflowRun": {
-                      "workflow": {
-                        "name": "CI"
-                      }
-                    }
-                  },
-                  "status": "COMPLETED",
-                  "conclusion": "SUCCESS",
-                  "url": "https://github.com/apache/tvm/runs/5694945392"
-                },
-                {
-                  "name": "cc-reviewers",
-                  "checkSuite": {
-                    "workflowRun": {
-                      "workflow": {
-                        "name": "PR"
-                      }
-                    }
-                  },
-                  "status": "COMPLETED",
-                  "conclusion": "SUCCESS",
-                  "url": "https://github.com/apache/tvm/runs/5694945029"
-                },
-                {
-                  "name": "tag-teams",
-                  "checkSuite": {
-                    "workflowRun": {
-                      "workflow": {
-                        "name": "Teams"
-                      }
-                    }
-                  },
-                  "status": "COMPLETED",
-                  "conclusion": "SUCCESS",
-                  "url": "https://github.com/apache/tvm/runs/5694945030"
-                },
-                {
-                  "name": "Windows",
-                  "checkSuite": {
-                    "workflowRun": {
-                      "workflow": {
-                        "name": "CI"
-                      }
-                    }
-                  },
-                  "status": "COMPLETED",
-                  "conclusion": "SUCCESS",
-                  "url": "https://github.com/apache/tvm/runs/5694945524"
-                },
-                {
-                  "state": "SUCCESS",
-                  "context": "tvm-ci/definitely-not-pr-head",
-                  "targetUrl": "https://ci.tlcpack.ai/job/tvm/job/PR-10786/1/display/redirect"
-                }
-              ]
-            }
-          }
-        }
-      }
-    ]
-  },
-  "reviewDecision": "APPROVED",
-  "reviews": {
-    "pageInfo": {
-      "hasPreviousPage": false
-    },
-    "nodes": [
-      {
-        "body": "@tvm-bot merge",
-        "updatedAt": "2022-03-25T22:13:50Z",
-        "authorCanPushToRepository": true,
-        "commit": {
-          "oid": "6f04bcf57d07f915a98fd91178f04d9e92a09fcd"
-        },
-        "author": {
-          "login": "kparzysz-quic"
-        },
-        "state": "APPROVED"
-      }
-    ]
-  }
-}
\ No newline at end of file
diff --git a/tests/python/ci/sample_prs/pr10786-nottriggered.json b/tests/python/ci/sample_prs/pr10786-nottriggered.json
deleted file mode 100644
index 0da541c434..0000000000
--- a/tests/python/ci/sample_prs/pr10786-nottriggered.json
+++ /dev/null
@@ -1,129 +0,0 @@
-{
-  "title": "[Hexagon] 2-d allocation cleanup",
-  "body": "- Added device validity check in allocation. HexagonDeviceAPI should only be called for CPU/Hexagon types.\r\n\r\n- Check for \"global.vtcm\" scope instead of \"vtcm\".  The ccope of N-d allocations produced by `LowerVtcmAlloc` should be `\"global.vtcm\"`.  The previous check allowed unsupported scope such as `\"local.vtcm\"`.\r\n\r\n- Remove `vtcmallocs` entry after calling free. Previously, the vtcm allocation map kept dangling pointers to `HexagonBuffer` objects after they  [...]
-  "state": "OPEN",
-  "author": {
-    "login": "abc"
-  },
-  "comments": {
-    "pageInfo": {
-      "hasPreviousPage": false
-    },
-    "nodes": []
-  },
-  "authorCommits": {
-    "nodes": [
-      {
-        "commit": {
-          "authors": {
-            "nodes": [
-              {
-                "name": "Eric Lunderberg",
-                "email": "elunderberg@octoml.ai"
-              },
-              {
-                "name": "Adam Straw",
-                "email": "astraw@octoml.ai"
-              }
-            ]
-          }
-        }
-      }
-    ]
-  },
-  "commits": {
-    "nodes": [
-      {
-        "commit": {
-          "oid": "6f04bcf57d07f915a98fd91178f04d9e92a09fcd",
-          "statusCheckRollup": {
-            "contexts": {
-              "pageInfo": {
-                "hasNextPage": false
-              },
-              "nodes": [
-                {
-                  "name": "MacOS",
-                  "checkSuite": {
-                    "workflowRun": {
-                      "workflow": {
-                        "name": "CI"
-                      }
-                    }
-                  },
-                  "status": "COMPLETED",
-                  "conclusion": "SUCCESS",
-                  "url": "https://github.com/apache/tvm/runs/5694945392"
-                },
-                {
-                  "name": "cc-reviewers",
-                  "checkSuite": {
-                    "workflowRun": {
-                      "workflow": {
-                        "name": "PR"
-                      }
-                    }
-                  },
-                  "status": "COMPLETED",
-                  "conclusion": "SUCCESS",
-                  "url": "https://github.com/apache/tvm/runs/5694945029"
-                },
-                {
-                  "name": "tag-teams",
-                  "checkSuite": {
-                    "workflowRun": {
-                      "workflow": {
-                        "name": "Teams"
-                      }
-                    }
-                  },
-                  "status": "COMPLETED",
-                  "conclusion": "SUCCESS",
-                  "url": "https://github.com/apache/tvm/runs/5694945030"
-                },
-                {
-                  "name": "Windows",
-                  "checkSuite": {
-                    "workflowRun": {
-                      "workflow": {
-                        "name": "CI"
-                      }
-                    }
-                  },
-                  "status": "COMPLETED",
-                  "conclusion": "SUCCESS",
-                  "url": "https://github.com/apache/tvm/runs/5694945524"
-                },
-                {
-                  "state": "SUCCESS",
-                  "context": "tvm-ci/pr-head",
-                  "targetUrl": "https://ci.tlcpack.ai/job/tvm/job/PR-10786/1/display/redirect"
-                }
-              ]
-            }
-          }
-        }
-      }
-    ]
-  },
-  "reviewDecision": "APPROVED",
-  "reviews": {
-    "pageInfo": {
-      "hasPreviousPage": false
-    },
-    "nodes": [
-      {
-        "body": "",
-        "updatedAt": "2022-03-25T22:13:50Z",
-        "authorCanPushToRepository": true,
-        "commit": {
-          "oid": "6f04bcf57d07f915a98fd91178f04d9e92a09fcd"
-        },
-        "author": {
-          "login": "kparzysz-quic"
-        },
-        "state": "APPROVED"
-      }
-    ]
-  }
-}
\ No newline at end of file
diff --git a/tests/python/ci/sample_prs/pr10786-oldreview.json b/tests/python/ci/sample_prs/pr10786-oldreview.json
deleted file mode 100644
index 1a2556cb6f..0000000000
--- a/tests/python/ci/sample_prs/pr10786-oldreview.json
+++ /dev/null
@@ -1,129 +0,0 @@
-{
-  "title": "[Hexagon] 2-d allocation cleanup",
-  "body": "- Added device validity check in allocation. HexagonDeviceAPI should only be called for CPU/Hexagon types.\r\n\r\n- Check for \"global.vtcm\" scope instead of \"vtcm\".  The ccope of N-d allocations produced by `LowerVtcmAlloc` should be `\"global.vtcm\"`.  The previous check allowed unsupported scope such as `\"local.vtcm\"`.\r\n\r\n- Remove `vtcmallocs` entry after calling free. Previously, the vtcm allocation map kept dangling pointers to `HexagonBuffer` objects after they  [...]
-  "state": "OPEN",
-  "author": {
-    "login": "abc"
-  },
-  "comments": {
-    "pageInfo": {
-      "hasPreviousPage": false
-    },
-    "nodes": []
-  },
-  "authorCommits": {
-    "nodes": [
-      {
-        "commit": {
-          "authors": {
-            "nodes": [
-              {
-                "name": "Eric Lunderberg",
-                "email": "elunderberg@octoml.ai"
-              },
-              {
-                "name": "Adam Straw",
-                "email": "astraw@octoml.ai"
-              }
-            ]
-          }
-        }
-      }
-    ]
-  },
-  "commits": {
-    "nodes": [
-      {
-        "commit": {
-          "oid": "6f04bcf57d07f915a98fd91178f04d9e92a09fcd",
-          "statusCheckRollup": {
-            "contexts": {
-              "pageInfo": {
-                "hasNextPage": false
-              },
-              "nodes": [
-                {
-                  "name": "MacOS",
-                  "checkSuite": {
-                    "workflowRun": {
-                      "workflow": {
-                        "name": "CI"
-                      }
-                    }
-                  },
-                  "status": "COMPLETED",
-                  "conclusion": "SUCCESS",
-                  "url": "https://github.com/apache/tvm/runs/5694945392"
-                },
-                {
-                  "name": "cc-reviewers",
-                  "checkSuite": {
-                    "workflowRun": {
-                      "workflow": {
-                        "name": "PR"
-                      }
-                    }
-                  },
-                  "status": "COMPLETED",
-                  "conclusion": "SUCCESS",
-                  "url": "https://github.com/apache/tvm/runs/5694945029"
-                },
-                {
-                  "name": "tag-teams",
-                  "checkSuite": {
-                    "workflowRun": {
-                      "workflow": {
-                        "name": "Teams"
-                      }
-                    }
-                  },
-                  "status": "COMPLETED",
-                  "conclusion": "SUCCESS",
-                  "url": "https://github.com/apache/tvm/runs/5694945030"
-                },
-                {
-                  "name": "Windows",
-                  "checkSuite": {
-                    "workflowRun": {
-                      "workflow": {
-                        "name": "CI"
-                      }
-                    }
-                  },
-                  "status": "COMPLETED",
-                  "conclusion": "SUCCESS",
-                  "url": "https://github.com/apache/tvm/runs/5694945524"
-                },
-                {
-                  "state": "SUCCESS",
-                  "context": "tvm-ci/pr-head",
-                  "targetUrl": "https://ci.tlcpack.ai/job/tvm/job/PR-10786/1/display/redirect"
-                }
-              ]
-            }
-          }
-        }
-      }
-    ]
-  },
-  "reviewDecision": "APPROVED",
-  "reviews": {
-    "pageInfo": {
-      "hasPreviousPage": false
-    },
-    "nodes": [
-      {
-        "body": "@tvm-bot merge",
-        "updatedAt": "2022-03-25T22:13:50Z",
-        "authorCanPushToRepository": true,
-        "commit": {
-          "oid": "abc12345"
-        },
-        "author": {
-          "login": "kparzysz-quic"
-        },
-        "state": "APPROVED"
-      }
-    ]
-  }
-}
\ No newline at end of file
diff --git a/tests/python/ci/sample_prs/pr10786-ignore-jobs.json b/tests/python/ci/sample_prs/pr10786.json
similarity index 78%
rename from tests/python/ci/sample_prs/pr10786-ignore-jobs.json
rename to tests/python/ci/sample_prs/pr10786.json
index dfcd806ff1..79f20ca609 100644
--- a/tests/python/ci/sample_prs/pr10786-ignore-jobs.json
+++ b/tests/python/ci/sample_prs/pr10786.json
@@ -1,6 +1,6 @@
 {
     "title": "[Hexagon] 2-d allocation cleanup",
-    "body": "- Added device validity check in allocation. HexagonDeviceAPI should only be called for CPU/Hexagon types.\r\n\r\n- Check for \"global.vtcm\" scope instead of \"vtcm\".  The ccope of N-d allocations produced by `LowerVtcmAlloc` should be `\"global.vtcm\"`.  The previous check allowed unsupported scope such as `\"local.vtcm\"`.\r\n\r\n- Remove `vtcmallocs` entry after calling free. Previously, the vtcm allocation map kept dangling pointers to `HexagonBuffer` objects after the [...]
+    "body": "- Added device validity check in allocation. HexagonDeviceAPI should only be called for CPU/Hexagon types.\r\n\r\n- Check for \"global.vtcm\" scope instead of \"vtcm\".  The ccope of N-d allocations produced by `LowerVtcmAlloc` should be `\"global.vtcm\"`.  The previous check allowed unsupported scope such as `\"local.vtcm\"`.\r\n\r\n- Remove `vtcmallocs` entry after calling free.\n\n\nThanks for contributing to TVM!   Please refer to guideline https://tvm.apache.org/docs/co [...]
     "state": "OPEN",
     "author": {
       "login": "abc"
@@ -65,7 +65,7 @@
                       }
                     },
                     "status": "COMPLETED",
-                    "conclusion": "FAILED",
+                    "conclusion": "SUCCESS",
                     "url": "https://github.com/apache/tvm/runs/5694945029"
                   },
                   {
@@ -119,7 +119,6 @@
           "commit": {
             "oid": "6f04bcf57d07f915a98fd91178f04d9e92a09fcd"
           },
-          "id": 123,
           "author": {
             "login": "kparzysz-quic"
           },
diff --git a/tests/python/ci/sample_prs/pr11244-unauthorized-comment.json b/tests/python/ci/sample_prs/pr11244-unauthorized-comment.json
deleted file mode 100644
index beafc05958..0000000000
--- a/tests/python/ci/sample_prs/pr11244-unauthorized-comment.json
+++ /dev/null
@@ -1,103 +0,0 @@
-{
-  "title": "[CRT runtime] Added functions TVMPlatformPreFuncCall and TVMPlatformPostFuncCall",
-  "body": "See [this thread ](https://discuss.tvm.apache.org/t/crt-add-platform-specific-pre-and-post-function-calls-in-crt-runtime/12723)for an explanation.",
-  "state": "OPEN",
-  "author": {
-    "login": "abc"
-  },
-  "comments": {
-    "pageInfo": {
-      "hasPreviousPage": false
-    },
-    "nodes": [
-      {
-        "authorAssociation": "NONE",
-        "author": {
-          "login": "abc"
-        },
-        "updatedAt": "2022-05-09T13:39:04Z",
-        "body": "@tvm-bot merge"
-      },
-      {
-        "authorAssociation": "CONTRIBUTOR",
-        "author": {
-          "login": "areusch"
-        },
-        "updatedAt": "2022-05-11T19:22:01Z",
-        "body": "i commented on the discuss forum thread. let's resolve there and then continue this PR."
-      }
-    ]
-  },
-  "authorCommits": {
-    "nodes": [
-      {
-        "commit": {
-          "authors": {
-            "nodes": [
-              {
-                "name": "Federico Peccia",
-                "email": "peccia@fzi.de"
-              }
-            ]
-          }
-        }
-      }
-    ]
-  },
-  "commits": {
-    "nodes": [
-      {
-        "commit": {
-          "oid": "79d355c5f837b3bdadb5d25b2a5d0d2802783ae2",
-          "statusCheckRollup": {
-            "contexts": {
-              "pageInfo": {
-                "hasNextPage": false
-              },
-              "nodes": [
-                {
-                  "name": "cc-reviewers",
-                  "checkSuite": {
-                    "workflowRun": {
-                      "workflow": {
-                        "name": "PR"
-                      }
-                    }
-                  },
-                  "status": "COMPLETED",
-                  "conclusion": "SUCCESS",
-                  "url": "https://github.com/apache/tvm/runs/6352791017"
-                },
-                {
-                  "name": "tag-teams",
-                  "checkSuite": {
-                    "workflowRun": {
-                      "workflow": {
-                        "name": "Teams"
-                      }
-                    }
-                  },
-                  "status": "COMPLETED",
-                  "conclusion": "SUCCESS",
-                  "url": "https://github.com/apache/tvm/runs/6352791014"
-                },
-                {
-                  "state": "ERROR",
-                  "context": "tvm-ci/pr-head",
-                  "targetUrl": "https://ci.tlcpack.ai/job/tvm/job/PR-11244/1/display/redirect"
-                }
-              ]
-            }
-          }
-        }
-      }
-    ]
-  },
-  "reviewDecision": "REVIEW_REQUIRED",
-  "reviews": {
-    "pageInfo": {
-      "hasPreviousPage": false
-    },
-    "nodes": []
-  }
-}
\ No newline at end of file
diff --git a/tests/python/ci/sample_prs/pr11267-no-review.json b/tests/python/ci/sample_prs/pr11267-no-review.json
deleted file mode 100644
index d2ad164673..0000000000
--- a/tests/python/ci/sample_prs/pr11267-no-review.json
+++ /dev/null
@@ -1,144 +0,0 @@
-{
-  "title": "[ci][docker] Use sccache everywhere by default",
-  "body": "This adds `/opt/sccache` to the PATH of each of the CI docker images so when cmake looks for a C compiler it will pick up the sccache wrapper by default. This fixes some issues where compiler invocations weren't being run though sccache. With this approach the invoker doesn't need to do anything specific to set up sccache.\n\nThis will require a follow up PR to update the Docker images and remove some of the sccache logic in `task_build.py`\n\n\n\ncc @Mousius @areusch",
-  "state": "OPEN",
-  "author": {
-    "login": "abc"
-  },
-  "comments": {
-    "pageInfo": {
-      "hasPreviousPage": false
-    },
-    "nodes": [
-      {
-        "authorAssociation": "CONTRIBUTOR",
-        "author": {
-          "login": "areusch"
-        },
-        "id": 124,
-        "updatedAt": "2022-05-11T16:54:32Z",
-        "body": "just confirming--we can disable this when doing a local build, correct? what's the mechanism by which we do that?"
-      },
-      {
-        "authorAssociation": "COLLABORATOR",
-        "author": {
-          "login": "driazati"
-        },
-        "id": 123,
-        "updatedAt": "2022-05-11T18:46:54Z",
-        "body": "@tvm-bot merge"
-      }
-    ]
-  },
-  "authorCommits": {
-    "nodes": [
-      {
-        "commit": {
-          "authors": {
-            "nodes": [
-              {
-                "name": "driazati",
-                "email": "driazati@users.noreply.github.com"
-              }
-            ]
-          }
-        }
-      }
-    ]
-  },
-  "commits": {
-    "nodes": [
-      {
-        "commit": {
-          "oid": "bb7f51d3e0fd50997012dfcce3c9b2b852cd3136",
-          "statusCheckRollup": {
-            "contexts": {
-              "pageInfo": {
-                "hasNextPage": false
-              },
-              "nodes": [
-                {
-                  "name": "MacOS",
-                  "checkSuite": {
-                    "workflowRun": {
-                      "workflow": {
-                        "name": "CI"
-                      }
-                    }
-                  },
-                  "status": "COMPLETED",
-                  "conclusion": "SUCCESS",
-                  "url": "https://github.com/apache/tvm/runs/6377784092"
-                },
-                {
-                  "name": "cc-reviewers",
-                  "checkSuite": {
-                    "workflowRun": {
-                      "workflow": {
-                        "name": "PR"
-                      }
-                    }
-                  },
-                  "status": "COMPLETED",
-                  "conclusion": "SUCCESS",
-                  "url": "https://github.com/apache/tvm/runs/6377778488"
-                },
-                {
-                  "name": "cc-reviewers",
-                  "checkSuite": {
-                    "workflowRun": {
-                      "workflow": {
-                        "name": "PR"
-                      }
-                    }
-                  },
-                  "status": "COMPLETED",
-                  "conclusion": "SUCCESS",
-                  "url": "https://github.com/apache/tvm/runs/6390508806"
-                },
-                {
-                  "name": "tag-teams",
-                  "checkSuite": {
-                    "workflowRun": {
-                      "workflow": {
-                        "name": "Teams"
-                      }
-                    }
-                  },
-                  "status": "COMPLETED",
-                  "conclusion": "SUCCESS",
-                  "url": "https://github.com/apache/tvm/runs/6390511833"
-                },
-                {
-                  "name": "Windows",
-                  "checkSuite": {
-                    "workflowRun": {
-                      "workflow": {
-                        "name": "CI"
-                      }
-                    }
-                  },
-                  "status": "COMPLETED",
-                  "conclusion": "SUCCESS",
-                  "url": "https://github.com/apache/tvm/runs/6377784248"
-                },
-                {
-                  "state": "SUCCESS",
-                  "context": "tvm-ci/pr-head",
-                  "targetUrl": "https://ci.tlcpack.ai/job/tvm/job/PR-11267/2/display/redirect"
-                }
-              ]
-            }
-          }
-        }
-      }
-    ]
-  },
-  "reviewDecision": "REVIEW_REQUIRED",
-  "reviews": {
-    "pageInfo": {
-      "hasPreviousPage": false
-    },
-    "nodes": []
-  }
-}
\ No newline at end of file
diff --git a/tests/python/ci/sample_prs/pr11442-rerun-ci.json b/tests/python/ci/sample_prs/pr11442-rerun-ci.json
deleted file mode 100644
index 0199b2921f..0000000000
--- a/tests/python/ci/sample_prs/pr11442-rerun-ci.json
+++ /dev/null
@@ -1,183 +0,0 @@
-{
-  "title": "Add 'static_library' runtime::Module",
-  "body": "(See https://discuss.tvm.apache.org/t/byoc-supporting-cutlass-byoc-with-collage/12796/6 for\r\ncontext, which in turn is part of Collage (https://github.com/apache/tvm-rfcs/blob/main/rfcs/0062-collage.md).\r\n\r\nThis adds a new 'DSO exportable' runtime module representing the contents of a .o file. It\r\nallows external codegen toolchains to yield a result which:\r\n - Like CSource modules, can be conveyed directly to the final export_library compilation\r\n   step for linkin [...]
-  "state": "OPEN",
-  "author": {
-    "login": "abc"
-  },
-  "comments": {
-    "pageInfo": {
-      "hasPreviousPage": false
-    },
-    "nodes": [
-      {
-        "authorAssociation": "MEMBER",
-        "author": {
-          "login": "tqchen"
-        },
-        "updatedAt": "2022-05-24T22:13:29Z",
-        "body": "Thanks @mbs-octoml  . I think we go with this as a temp workaround with a mind that the IsDSOExportable and ImplementsFunction likely should go to Artifact."
-      },
-      {
-        "authorAssociation": "CONTRIBUTOR",
-        "author": {
-          "login": "mbs-octoml"
-        },
-        "updatedAt": "2022-05-24T22:56:07Z",
-        "body": "Yeah, we really need to put some love into that.\r\n\r\nCollecting all the pieces needed for deployment along with their metadata a la Artifact is pretty clearly needed, though I suspect that will need to be abstract to cover the spectrum from firmware image to dynamically loadable .so to ready-to-call JITed code to tar.\r\n\r\nI can't help thinking we should also think about build rules guarded by target kinds & attributes, since again there's just so may ways to proceed."
-      },
-      {
-        "authorAssociation": "MEMBER",
-        "author": {
-          "login": "tqchen"
-        },
-        "updatedAt": "2022-05-24T23:08:00Z",
-        "body": "Perhaps we will end up building our own cmake/bazel :p in another time"
-      },
-      {
-        "authorAssociation": "CONTRIBUTOR",
-        "author": {
-          "login": "mbs-octoml"
-        },
-        "updatedAt": "2022-05-25T22:11:44Z",
-        "body": "Thanks Tianqi. Let's see if  this new fancy bot works...\r\n\r\n"
-      },
-      {
-        "authorAssociation": "CONTRIBUTOR",
-        "author": {
-          "login": "mbs-octoml"
-        },
-        "updatedAt": "2022-05-25T22:11:50Z",
-        "body": "@tvm-bot merge"
-      },
-      {
-        "authorAssociation": "NONE",
-        "author": {
-          "login": "github-actions"
-        },
-        "updatedAt": "2022-05-25T22:12:10Z",
-        "body": "Cannot merge, did not find any approving reviews from users with write access on 96d4e62da5a7b78da18d0ee28cc6261d8fbf31c4"
-      },
-      {
-        "authorAssociation": "CONTRIBUTOR",
-        "author": {
-          "login": "mbs-octoml"
-        },
-        "updatedAt": "2022-05-25T22:12:37Z",
-        "body": "@tvm-bot rerun"
-      }
-    ]
-  },
-  "authorCommits": {
-    "nodes": [
-      {
-        "commit": {
-          "authors": {
-            "nodes": [
-              {
-                "name": "mbs-octoml",
-                "email": "mbs@octoml.ai"
-              }
-            ]
-          }
-        }
-      }
-    ]
-  },
-  "commits": {
-    "nodes": [
-      {
-        "commit": {
-          "oid": "96d4e62da5a7b78da18d0ee28cc6261d8fbf31c4",
-          "statusCheckRollup": {
-            "contexts": {
-              "pageInfo": {
-                "hasNextPage": false
-              },
-              "nodes": [
-                {
-                  "name": "MacOS",
-                  "checkSuite": {
-                    "workflowRun": {
-                      "workflow": {
-                        "name": "CI"
-                      }
-                    }
-                  },
-                  "status": "COMPLETED",
-                  "conclusion": "SUCCESS",
-                  "url": "https://github.com/apache/tvm/runs/6598275844"
-                },
-                {
-                  "name": "cc-reviewers",
-                  "checkSuite": {
-                    "workflowRun": {
-                      "workflow": {
-                        "name": "PR"
-                      }
-                    }
-                  },
-                  "status": "COMPLETED",
-                  "conclusion": "SUCCESS",
-                  "url": "https://github.com/apache/tvm/runs/6598273162"
-                },
-                {
-                  "name": "Windows",
-                  "checkSuite": {
-                    "workflowRun": {
-                      "workflow": {
-                        "name": "CI"
-                      }
-                    }
-                  },
-                  "status": "COMPLETED",
-                  "conclusion": "SUCCESS",
-                  "url": "https://github.com/apache/tvm/runs/6598275717"
-                },
-                {
-                  "name": "Android",
-                  "checkSuite": {
-                    "workflowRun": {
-                      "workflow": {
-                        "name": "CI"
-                      }
-                    }
-                  },
-                  "status": "COMPLETED",
-                  "conclusion": "SUCCESS",
-                  "url": "https://github.com/apache/tvm/runs/6598275593"
-                },
-                {
-                  "state": "SUCCESS",
-                  "context": "tvm-ci/pr-head",
-                  "targetUrl": "https://ci.tlcpack.ai/job/tvm/job/PR-11442/4/display/redirect"
-                }
-              ]
-            }
-          }
-        }
-      }
-    ]
-  },
-  "reviewDecision": "APPROVED",
-  "reviews": {
-    "pageInfo": {
-      "hasPreviousPage": false
-    },
-    "nodes": [
-      {
-        "body": "",
-        "updatedAt": "2022-05-24T23:08:31Z",
-        "url": "https://github.com/apache/tvm/pull/11442#pullrequestreview-983954561",
-        "authorCanPushToRepository": true,
-        "commit": {
-          "oid": "23c600097cf1c2a55acda059626a060e106dd023"
-        },
-        "author": {
-          "login": "tqchen"
-        },
-        "state": "APPROVED"
-      }
-    ]
-  }
-}
\ No newline at end of file
diff --git a/tests/python/ci/test_ci.py b/tests/python/ci/test_ci.py
index 0939aae10a..f2e686d1e5 100644
--- a/tests/python/ci/test_ci.py
+++ b/tests/python/ci/test_ci.py
@@ -23,15 +23,14 @@ from pathlib import Path
 
 import pytest
 import tvm.testing
-from .test_utils import REPO_ROOT, TempGit
+from .test_utils import REPO_ROOT, TempGit, run_script
 
 
-def parameterize_named(*values):
-    keys = list(values[0].keys())
-    if len(keys) == 1:
-        return pytest.mark.parametrize(",".join(keys), [d[keys[0]] for d in values])
-
-    return pytest.mark.parametrize(",".join(keys), [tuple(d.values()) for d in values])
+def parameterize_named(**kwargs):
+    keys = next(iter(kwargs.values())).keys()
+    return pytest.mark.parametrize(
+        ",".join(keys), [tuple(d.values()) for d in kwargs.values()], ids=kwargs.keys()
+    )
 
 
 # pylint: disable=line-too-long
@@ -137,23 +136,7 @@ TEST_DATA_SKIPPED_BOT = {
 
 
 @tvm.testing.skip_if_wheel_test
-@pytest.mark.parametrize(
-    [
-        "main_xml_file",
-        "main_xml_content",
-        "pr_xml_file",
-        "pr_xml_content",
-        "target_url",
-        "s3_prefix",
-        "jenkins_prefix",
-        "common_main_build",
-        "commit_sha",
-        "expected_url",
-        "expected_body",
-    ],
-    [tuple(d.values()) for d in TEST_DATA_SKIPPED_BOT.values()],
-    ids=TEST_DATA_SKIPPED_BOT.keys(),
-)
+@parameterize_named(**TEST_DATA_SKIPPED_BOT)
 # pylint: enable=line-too-long
 def test_skipped_tests_comment(
     tmpdir_factory,
@@ -182,49 +165,37 @@ def test_skipped_tests_comment(
             f.write(textwrap.dedent(xml_content))
 
     git = TempGit(tmpdir_factory.mktemp("tmp_git_dir"))
-    git.run("init")
-    git.run("checkout", "-b", "main")
-    git.run("remote", "add", "origin", "https://github.com/apache/tvm.git")
 
     pr_test_report_dir = Path(git.cwd) / "pr-reports"
     write_xml_file(pr_test_report_dir, pr_xml_file, pr_xml_content)
     main_test_report_dir = Path(git.cwd) / "main-reports"
     write_xml_file(main_test_report_dir, main_xml_file, main_xml_content)
 
-    proc = subprocess.run(
+    proc = run_script(
         [
-            str(skipped_tests_script),
+            skipped_tests_script,
             "--dry-run",
             f"--s3-prefix={s3_prefix}",
             f"--jenkins-prefix={jenkins_prefix}",
             f"--common-main-build={common_main_build}",
         ],
-        stdout=subprocess.PIPE,
-        stderr=subprocess.PIPE,
         env={"TARGET_URL": target_url, "COMMIT_SHA": commit_sha},
-        encoding="utf-8",
         cwd=git.cwd,
-        check=False,
     )
-    if proc.returncode != 0:
-        raise RuntimeError(f"Process failed:\nstdout:\n{proc.stdout}\n\nstderr:\n{proc.stderr}")
 
-    assert f"Dry run, would have posted {expected_url} with data {expected_body}." in proc.stderr
+    assert_in(f"Dry run, would have posted {expected_url} with data {expected_body}.", proc.stderr)
 
 
 @tvm.testing.skip_if_wheel_test
-@pytest.mark.parametrize(
-    "target_url,base_url,commit_sha,expected_url,expected_body",
-    [
-        (
-            "https://ci.tlcpack.ai/job/tvm/job/PR-11594/3/display/redirect",
-            "https://pr-docs.tlcpack.ai",
-            "SHA",
-            "issues/11594/comments",
-            "<!---docs-bot-comment-->\n\nBuilt docs for commit SHA can be found "
-            "[here](https://pr-docs.tlcpack.ai/PR-11594/3/docs/index.html).",
-        )
-    ],
+@parameterize_named(
+    doc_link=dict(
+        target_url="https://ci.tlcpack.ai/job/tvm/job/PR-11594/3/display/redirect",
+        base_url="https://pr-docs.tlcpack.ai",
+        commit_sha="SHA",
+        expected_url="issues/11594/comments",
+        expected_body="<!---docs-bot-comment-->\n\nBuilt docs for commit SHA can be found "
+        "[here](https://pr-docs.tlcpack.ai/PR-11594/3/docs/index.html).",
+    )
 )
 def test_docs_comment(
     tmpdir_factory, target_url, base_url, commit_sha, expected_url, expected_body
@@ -235,146 +206,93 @@ def test_docs_comment(
     docs_comment_script = REPO_ROOT / "ci" / "scripts" / "github_docs_comment.py"
 
     git = TempGit(tmpdir_factory.mktemp("tmp_git_dir"))
-    git.run("init")
-    git.run("checkout", "-b", "main")
-    git.run("remote", "add", "origin", "https://github.com/apache/tvm.git")
-    proc = subprocess.run(
-        [str(docs_comment_script), "--dry-run", f"--base-url-docs={base_url}"],
-        stdout=subprocess.PIPE,
-        stderr=subprocess.PIPE,
+    proc = run_script(
+        [docs_comment_script, "--dry-run", f"--base-url-docs={base_url}"],
         env={"TARGET_URL": target_url, "COMMIT_SHA": commit_sha},
-        encoding="utf-8",
         cwd=git.cwd,
-        check=False,
     )
-    if proc.returncode != 0:
-        raise RuntimeError(f"Process failed:\nstdout:\n{proc.stdout}\n\nstderr:\n{proc.stderr}")
 
-    assert f"Dry run, would have posted {expected_url} with data {expected_body}." in proc.stderr
+    assert_in(f"Dry run, would have posted {expected_url} with data {expected_body}.", proc.stderr)
 
 
 @tvm.testing.skip_if_wheel_test
-def test_cc_reviewers(tmpdir_factory):
-    """
-    Test that reviewers are added from 'cc @someone' messages in PRs
-    """
-    reviewers_script = REPO_ROOT / "ci" / "scripts" / "github_cc_reviewers.py"
-
-    def run(pr_body, requested_reviewers, existing_review_users, expected_reviewers):
-        git = TempGit(tmpdir_factory.mktemp("tmp_git_dir"))
-        git.run("init")
-        git.run("checkout", "-b", "main")
-        git.run("remote", "add", "origin", "https://github.com/apache/tvm.git")
-        reviews = [{"user": {"login": r}} for r in existing_review_users]
-        requested_reviewers = [{"login": r} for r in requested_reviewers]
-        proc = subprocess.run(
-            [str(reviewers_script), "--dry-run", "--testing-reviews-json", json.dumps(reviews)],
-            stdout=subprocess.PIPE,
-            stderr=subprocess.PIPE,
-            env={
-                "PR": json.dumps(
-                    {"number": 1, "body": pr_body, "requested_reviewers": requested_reviewers}
-                )
-            },
-            encoding="utf-8",
-            cwd=git.cwd,
-            check=False,
-        )
-        if proc.returncode != 0:
-            raise RuntimeError(f"Process failed:\nstdout:\n{proc.stdout}\n\nstderr:\n{proc.stderr}")
-
-        assert f"After filtering existing reviewers, adding: {expected_reviewers}" in proc.stdout
-
-    run(pr_body="abc", requested_reviewers=[], existing_review_users=[], expected_reviewers=[])
-    run(
+@parameterize_named(
+    cc_no_one=dict(
+        pr_body="abc", requested_reviewers=[], existing_review_users=[], expected_reviewers=[]
+    ),
+    cc_abc=dict(
         pr_body="cc @abc",
         requested_reviewers=[],
         existing_review_users=[],
         expected_reviewers=["abc"],
-    )
-    run(pr_body="cc @", requested_reviewers=[], existing_review_users=[], expected_reviewers=[])
-    run(
+    ),
+    bad_cc_line=dict(
+        pr_body="cc @", requested_reviewers=[], existing_review_users=[], expected_reviewers=[]
+    ),
+    cc_multiple=dict(
         pr_body="cc @abc @def",
         requested_reviewers=[],
         existing_review_users=[],
         expected_reviewers=["abc", "def"],
-    )
-    run(
+    ),
+    with_existing=dict(
         pr_body="some text cc @abc @def something else",
         requested_reviewers=[],
         existing_review_users=[],
         expected_reviewers=["abc", "def"],
-    )
-    run(
+    ),
+    with_existing_split=dict(
         pr_body="some text cc @abc @def something else\n\n another cc @zzz z",
         requested_reviewers=[],
         existing_review_users=[],
         expected_reviewers=["abc", "def", "zzz"],
-    )
-    run(
+    ),
+    with_existing_request=dict(
         pr_body="some text cc @abc @def something else\n\n another cc @zzz z",
         requested_reviewers=["abc"],
         existing_review_users=[],
         expected_reviewers=["def", "zzz"],
-    )
-    run(
+    ),
+    with_existing_reviewers=dict(
         pr_body="some text cc @abc @def something else\n\n another cc @zzz z",
         requested_reviewers=["abc"],
         existing_review_users=["abc"],
         expected_reviewers=["def", "zzz"],
-    )
-    run(
+    ),
+    with_no_reviewers=dict(
         pr_body="some text cc @abc @def something else\n\n another cc @zzz z",
         requested_reviewers=[],
         existing_review_users=["abc"],
         expected_reviewers=["def", "zzz"],
-    )
-
-
-def test_update_branch(tmpdir_factory):
+    ),
+)
+def test_cc_reviewers(
+    tmpdir_factory, pr_body, requested_reviewers, existing_review_users, expected_reviewers
+):
     """
-    Test that the last-successful branch script updates successfully
+    Test that reviewers are added from 'cc @someone' messages in PRs
     """
-    update_script = REPO_ROOT / "ci" / "scripts" / "update_branch.py"
-
-    def run(statuses, expected_rc, expected_output):
-        git = TempGit(tmpdir_factory.mktemp("tmp_git_dir"))
-        git.run("init")
-        git.run("checkout", "-b", "main")
-        git.run("remote", "add", "origin", "https://github.com/apache/tvm.git")
-        commit = {
-            "statusCheckRollup": {"contexts": {"nodes": statuses}},
-            "oid": "123",
-            "messageHeadline": "hello",
-        }
-        data = {
-            "data": {
-                "repository": {
-                    "defaultBranchRef": {"target": {"history": {"edges": [], "nodes": [commit]}}}
-                }
-            }
-        }
-        proc = subprocess.run(
-            [str(update_script), "--dry-run", "--testonly-json", json.dumps(data)],
-            stdout=subprocess.PIPE,
-            stderr=subprocess.PIPE,
-            encoding="utf-8",
-            cwd=git.cwd,
-            check=False,
-        )
+    reviewers_script = REPO_ROOT / "ci" / "scripts" / "github_cc_reviewers.py"
 
-        if proc.returncode != expected_rc:
-            raise RuntimeError(
-                f"Wrong return code:\nstdout:\n{proc.stdout}\n\nstderr:\n{proc.stderr}"
+    git = TempGit(tmpdir_factory.mktemp("tmp_git_dir"))
+    reviews = [{"user": {"login": r}} for r in existing_review_users]
+    requested_reviewers = [{"login": r} for r in requested_reviewers]
+    proc = run_script(
+        [reviewers_script, "--dry-run", "--testing-reviews-json", json.dumps(reviews)],
+        env={
+            "PR": json.dumps(
+                {"number": 1, "body": pr_body, "requested_reviewers": requested_reviewers}
             )
+        },
+        cwd=git.cwd,
+    )
 
-        if expected_output not in proc.stdout:
-            raise RuntimeError(
-                f"Missing {expected_output}:\nstdout:\n{proc.stdout}\n\nstderr:\n{proc.stderr}"
-            )
+    assert f"After filtering existing reviewers, adding: {expected_reviewers}" in proc.stdout
 
+
+@parameterize_named(
     # Missing expected tvm-ci/branch test
-    run(
+    missing_tvm_ci_branch=dict(
         statuses=[
             {
                 "context": "test",
@@ -383,10 +301,9 @@ def test_update_branch(tmpdir_factory):
         ],
         expected_rc=1,
         expected_output="No good commits found in the last 1 commits",
-    )
-
+    ),
     # Only has the right passing test
-    run(
+    has_expected_test=dict(
         statuses=[
             {
                 "context": "tvm-ci/branch",
@@ -395,10 +312,9 @@ def test_update_branch(tmpdir_factory):
         ],
         expected_rc=0,
         expected_output="Found last good commit: 123: hello",
-    )
-
+    ),
     # Check with many statuses
-    run(
+    many_statuses=dict(
         statuses=[
             {
                 "context": "tvm-ci/branch",
@@ -415,8 +331,8 @@ def test_update_branch(tmpdir_factory):
         ],
         expected_rc=1,
         expected_output="No good commits found in the last 1 commits",
-    )
-    run(
+    ),
+    many_success_statuses=dict(
         statuses=[
             {
                 "context": "tvm-ci/branch",
@@ -433,17 +349,50 @@ def test_update_branch(tmpdir_factory):
         ],
         expected_rc=0,
         expected_output="Found last good commit: 123: hello",
+    ),
+)
+def test_update_branch(tmpdir_factory, statuses, expected_rc, expected_output):
+    """
+    Test that the last-successful branch script updates successfully
+    """
+    update_script = REPO_ROOT / "ci" / "scripts" / "update_branch.py"
+
+    git = TempGit(tmpdir_factory.mktemp("tmp_git_dir"))
+    commit = {
+        "statusCheckRollup": {"contexts": {"nodes": statuses}},
+        "oid": "123",
+        "messageHeadline": "hello",
+    }
+    data = {
+        "data": {
+            "repository": {
+                "defaultBranchRef": {"target": {"history": {"edges": [], "nodes": [commit]}}}
+            }
+        }
+    }
+    proc = run_script(
+        [update_script, "--dry-run", "--testonly-json", json.dumps(data)],
+        cwd=git.cwd,
+        check=False,
     )
 
+    if proc.returncode != expected_rc:
+        raise RuntimeError(f"Wrong return code:\nstdout:\n{proc.stdout}\n\nstderr:\n{proc.stderr}")
+
+    if expected_output not in proc.stdout:
+        raise RuntimeError(
+            f"Missing {expected_output}:\nstdout:\n{proc.stdout}\n\nstderr:\n{proc.stderr}"
+        )
+
 
 @parameterize_named(
-    dict(
+    dont_skip_main=dict(
         commands=[],
         should_skip=False,
         pr_title="[skip ci] test",
         why="ci should not be skipped on main",
     ),
-    dict(
+    dont_skip_main_with_commit=dict(
         commands=[
             ["commit", "--allow-empty", "--message", "[skip ci] commit 1"],
         ],
@@ -451,7 +400,7 @@ def test_update_branch(tmpdir_factory):
         pr_title="[skip ci] test",
         why="ci should not be skipped on main",
     ),
-    dict(
+    skip_on_new_branch=dict(
         commands=[
             ["checkout", "-b", "some_new_branch"],
             ["commit", "--allow-empty", "--message", "[skip ci] commit 1"],
@@ -460,7 +409,7 @@ def test_update_branch(tmpdir_factory):
         pr_title="[skip ci] test",
         why="ci should be skipped on a branch with [skip ci] in the last commit",
     ),
-    dict(
+    no_skip_in_pr_title=dict(
         commands=[
             ["checkout", "-b", "some_new_branch"],
             ["commit", "--allow-empty", "--message", "[skip ci] commit 1"],
@@ -470,7 +419,7 @@ def test_update_branch(tmpdir_factory):
         why="ci should not be skipped on a branch with "
         "[skip ci] in the last commit but not the PR title",
     ),
-    dict(
+    skip_in_pr_title=dict(
         commands=[
             ["checkout", "-b", "some_new_branch"],
             ["commit", "--allow-empty", "--message", "[skip ci] commit 1"],
@@ -480,17 +429,7 @@ def test_update_branch(tmpdir_factory):
         pr_title="[skip ci] test",
         why="ci should be skipped with [skip ci] in the PR title",
     ),
-    dict(
-        commands=[
-            ["checkout", "-b", "some_new_branch"],
-            ["commit", "--allow-empty", "--message", "[skip ci] commit 1"],
-            ["commit", "--allow-empty", "--message", "commit 2"],
-        ],
-        should_skip=True,
-        pr_title="[skip ci] test",
-        why="ci should be skipped with [skip ci] in the PR title",
-    ),
-    dict(
+    skip_in_pr_title_many_commits=dict(
         commands=[
             ["checkout", "-b", "some_new_branch"],
             ["commit", "--allow-empty", "--message", "commit 1"],
@@ -502,7 +441,7 @@ def test_update_branch(tmpdir_factory):
         pr_title="[skip ci] test",
         why="ci should be skipped with [skip ci] in the PR title",
     ),
-    dict(
+    skip_anywhere_in_title=dict(
         commands=[
             ["checkout", "-b", "some_new_branch"],
         ],
@@ -518,22 +457,16 @@ def test_skip_ci(tmpdir_factory, commands, should_skip, pr_title, why):
     skip_ci_script = REPO_ROOT / "ci" / "scripts" / "git_skip_ci.py"
 
     git = TempGit(tmpdir_factory.mktemp("tmp_git_dir"))
-    # Jenkins git is too old and doesn't have 'git init --initial-branch'
-    git.run("init")
-    git.run("checkout", "-b", "main")
-    git.run("remote", "add", "origin", "https://github.com/apache/tvm.git")
+
     git.run("config", "user.name", "ci")
     git.run("config", "user.email", "email@example.com")
     git.run("commit", "--allow-empty", "--message", "base commit")
     for command in commands:
         git.run(*command)
     pr_number = "1234"
-    proc = subprocess.run(
-        [str(skip_ci_script), "--pr", pr_number, "--pr-title", pr_title],
+    proc = run_script(
+        [skip_ci_script, "--pr", pr_number, "--pr-title", pr_title],
         cwd=git.cwd,
-        stderr=subprocess.STDOUT,
-        stdout=subprocess.PIPE,
-        encoding="utf-8",
         check=False,
     )
     expected = 0 if should_skip else 1
@@ -544,120 +477,66 @@ def test_skip_ci(tmpdir_factory, commands, should_skip, pr_title, why):
         )
 
 
-def test_skip_globs(tmpdir_factory):
+@parameterize_named(
+    no_file=dict(files=[], should_skip=True),
+    readme=dict(files=["README.md"], should_skip=True),
+    c_file=dict(files=["test.c"], should_skip=False),
+    c_and_readme=dict(files=["test.c", "README.md"], should_skip=False),
+    src_file_and_readme=dict(
+        files=["src/autotvm/feature_visitor.cc", "README.md"], should_skip=False
+    ),
+    yaml_and_readme=dict(files=[".asf.yaml", "docs/README.md"], should_skip=True),
+)
+def test_skip_globs(tmpdir_factory, files, should_skip):
     """
     Test that CI is skipped if only certain files are edited
     """
     script = REPO_ROOT / "ci" / "scripts" / "git_skip_ci_globs.py"
 
-    def run(files, should_skip):
-        git = TempGit(tmpdir_factory.mktemp("tmp_git_dir"))
-        # Jenkins git is too old and doesn't have 'git init --initial-branch'
-        git.run("init")
-        git.run("checkout", "-b", "main")
-        git.run("remote", "add", "origin", "https://github.com/apache/tvm.git")
-
-        proc = subprocess.run(
-            [
-                str(script),
-                "--files",
-                ",".join(files),
-            ],
-            stdout=subprocess.PIPE,
-            stderr=subprocess.PIPE,
-            encoding="utf-8",
-            cwd=git.cwd,
-            check=False,
-        )
-
-        if should_skip:
-            assert proc.returncode == 0
-        else:
-            assert proc.returncode == 1
-
-    run([], should_skip=True)
-    run(["README.md"], should_skip=True)
-    run(["test.c"], should_skip=False)
-    run(["test.c", "README.md"], should_skip=False)
-    run(["src/autotvm/feature_visitor.cc", "README.md"], should_skip=False)
-    run([".asf.yaml", "docs/README.md"], should_skip=True)
+    git = TempGit(tmpdir_factory.mktemp("tmp_git_dir"))
 
+    proc = run_script(
+        [
+            script,
+            "--files",
+            ",".join(files),
+        ],
+        check=False,
+        cwd=git.cwd,
+    )
 
-def test_ping_reviewers(tmpdir_factory):
-    """
-    Test that reviewers are messaged after a time period of inactivity
-    """
-    reviewers_script = REPO_ROOT / "ci" / "scripts" / "ping_reviewers.py"
+    if should_skip:
+        assert proc.returncode == 0
+    else:
+        assert proc.returncode == 1
 
-    def run(pull_request, check):
-        git = TempGit(tmpdir_factory.mktemp("tmp_git_dir"))
-        # Jenkins git is too old and doesn't have 'git init --initial-branch'
-        git.run("init")
-        git.run("checkout", "-b", "main")
-        git.run("remote", "add", "origin", "https://github.com/apache/tvm.git")
-
-        data = {
-            "data": {
-                "repository": {
-                    "pullRequests": {
-                        "nodes": [pull_request],
-                        "edges": [],
-                    }
-                }
-            }
-        }
-        proc = subprocess.run(
-            [
-                str(reviewers_script),
-                "--dry-run",
-                "--wait-time-minutes",
-                "1",
-                "--cutoff-pr-number",
-                "5",
-                "--allowlist",
-                "user",
-                "--pr-json",
-                json.dumps(data),
-                "--now",
-                "2022-01-26T17:54:19Z",
-            ],
-            stdout=subprocess.PIPE,
-            stderr=subprocess.PIPE,
-            encoding="utf-8",
-            cwd=git.cwd,
-            check=False,
-        )
-        if proc.returncode != 0:
-            raise RuntimeError(f"Process failed:\nstdout:\n{proc.stdout}\n\nstderr:\n{proc.stderr}")
 
-        assert check in proc.stdout
+def all_time_keys(time):
+    return {
+        "updatedAt": time,
+        "lastEditedAt": time,
+        "createdAt": time,
+        "publishedAt": time,
+    }
 
-    def all_time_keys(time):
-        return {
-            "updatedAt": time,
-            "lastEditedAt": time,
-            "createdAt": time,
-            "publishedAt": time,
-        }
 
-    run(
-        {
+@parameterize_named(
+    draft=dict(
+        pull_request={
             "isDraft": True,
             "number": 2,
         },
-        "Checking 0 of 1 fetched",
-    )
-
-    run(
-        {
+        check="Checking 0 of 1 fetched",
+    ),
+    not_draft=dict(
+        pull_request={
             "isDraft": False,
             "number": 2,
         },
-        "Checking 0 of 1 fetched",
-    )
-
-    run(
-        {
+        check="Checking 0 of 1 fetched",
+    ),
+    week_old=dict(
+        pull_request={
             "number": 123,
             "url": "https://github.com/apache/tvm/pull/123",
             "body": "cc @someone",
@@ -667,12 +546,11 @@ def test_ping_reviewers(tmpdir_factory):
             **all_time_keys("2022-01-18T17:54:19Z"),
             "comments": {"nodes": []},
         },
-        "Pinging reviewers ['someone'] on https://github.com/apache/tvm/pull/123",
-    )
-
+        check="Pinging reviewers ['someone'] on https://github.com/apache/tvm/pull/123",
+    ),
     # Check allowlist functionality
-    run(
-        {
+    allowlist=dict(
+        pull_request={
             "number": 123,
             "url": "https://github.com/apache/tvm/pull/123",
             "body": "cc @someone",
@@ -686,12 +564,11 @@ def test_ping_reviewers(tmpdir_factory):
                 ]
             },
         },
-        "Checking 0 of 1 fetched",
-    )
-
+        check="Checking 0 of 1 fetched",
+    ),
     # Old comment, ping
-    run(
-        {
+    old_comment=dict(
+        pull_request={
             "number": 123,
             "url": "https://github.com/apache/tvm/pull/123",
             "body": "cc @someone",
@@ -708,12 +585,11 @@ def test_ping_reviewers(tmpdir_factory):
                 ]
             },
         },
-        "Pinging reviewers ['someone'] on https://github.com/apache/tvm/pull/123",
-    )
-
+        check="Pinging reviewers ['someone'] on https://github.com/apache/tvm/pull/123",
+    ),
     # New comment, don't ping
-    run(
-        {
+    new_comment=dict(
+        pull_request={
             "number": 123,
             "url": "https://github.com/apache/tvm/pull/123",
             "body": "cc @someone",
@@ -727,8 +603,45 @@ def test_ping_reviewers(tmpdir_factory):
                 ]
             },
         },
-        "Not pinging PR 123",
+        check="Not pinging PR 123",
+    ),
+)
+def test_ping_reviewers(tmpdir_factory, pull_request, check):
+    """
+    Test that reviewers are messaged after a time period of inactivity
+    """
+    reviewers_script = REPO_ROOT / "ci" / "scripts" / "ping_reviewers.py"
+
+    git = TempGit(tmpdir_factory.mktemp("tmp_git_dir"))
+
+    data = {
+        "data": {
+            "repository": {
+                "pullRequests": {
+                    "nodes": [pull_request],
+                    "edges": [],
+                }
+            }
+        }
+    }
+    proc = run_script(
+        [
+            reviewers_script,
+            "--dry-run",
+            "--wait-time-minutes",
+            "1",
+            "--cutoff-pr-number",
+            "5",
+            "--allowlist",
+            "user",
+            "--pr-json",
+            json.dumps(data),
+            "--now",
+            "2022-01-26T17:54:19Z",
+        ],
+        cwd=git.cwd,
     )
+    assert_in(check, proc.stdout)
 
 
 def assert_in(needle: str, haystack: str):
@@ -740,69 +653,8 @@ def assert_in(needle: str, haystack: str):
 
 
 @tvm.testing.skip_if_wheel_test
-def test_github_tag_teams(tmpdir_factory):
-    """
-    Check that individuals are tagged from team headers
-    """
-    tag_script = REPO_ROOT / "ci" / "scripts" / "github_tag_teams.py"
-
-    def run(source_type, data, check):
-        git = TempGit(tmpdir_factory.mktemp("tmp_git_dir"))
-        git.run("init")
-        git.run("checkout", "-b", "main")
-        git.run("remote", "add", "origin", "https://github.com/apache/tvm.git")
-
-        issue_body = """
-        some text
-        [temporary] opt-in: @person5
-
-        - something: @person1 @person2
-        - something3: @person1 @person2 @SOME1-ONE-
-        - something else @person1 @person2
-        - something else2: @person1 @person2
-        - something-else @person1 @person2
-        """
-        comment1 = """
-        another thing: @person3
-        another-thing @person3
-        """
-        comment2 = """
-        something @person4
-        @person5
-        """
-        teams = {
-            "data": {
-                "repository": {
-                    "issue": {
-                        "body": issue_body,
-                        "comments": {"nodes": [{"body": comment1}, {"body": comment2}]},
-                    }
-                }
-            }
-        }
-        env = {
-            source_type: json.dumps(data),
-        }
-        proc = subprocess.run(
-            [
-                str(tag_script),
-                "--dry-run",
-                "--team-issue-json",
-                json.dumps(teams),
-            ],
-            stdout=subprocess.PIPE,
-            stderr=subprocess.PIPE,
-            encoding="utf-8",
-            cwd=git.cwd,
-            env=env,
-            check=False,
-        )
-        if proc.returncode != 0:
-            raise RuntimeError(f"Process failed:\nstdout:\n{proc.stdout}\n\nstderr:\n{proc.stderr}")
-
-        assert_in(check, proc.stdout)
-
-    run(
+@parameterize_named(
+    no_cc=dict(
         source_type="ISSUE",
         data={
             "title": "A title",
@@ -818,9 +670,8 @@ def test_github_tag_teams(tmpdir_factory):
             ),
         },
         check="No one to cc, exiting",
-    )
-
-    run(
+    ),
+    no_additional_cc=dict(
         source_type="ISSUE",
         data={
             "title": "A title",
@@ -838,9 +689,8 @@ def test_github_tag_teams(tmpdir_factory):
             ),
         },
         check="No one to cc, exiting",
-    )
-
-    run(
+    ),
+    cc_update=dict(
         source_type="ISSUE",
         data={
             "title": "A title",
@@ -858,9 +708,8 @@ def test_github_tag_teams(tmpdir_factory):
         },
         check="would have updated issues/1234 with {'body': "
         "'\\nhello\\n\\nsomething\\n\\ncc @person1 @person2 @person4'}",
-    )
-
-    run(
+    ),
+    already_cced=dict(
         source_type="ISSUE",
         data={
             "title": "A title",
@@ -877,9 +726,8 @@ def test_github_tag_teams(tmpdir_factory):
             ),
         },
         check="No one to cc, exiting",
-    )
-
-    run(
+    ),
+    not_already_cced=dict(
         source_type="ISSUE",
         data={
             "title": "[something] A title",
@@ -897,9 +745,8 @@ def test_github_tag_teams(tmpdir_factory):
         },
         check="would have updated issues/1234 with {'body': "
         "'\\nhello\\n\\nsomething\\n\\ncc @person1 @person2 @person4'}",
-    )
-
-    run(
+    ),
+    no_new_ccs=dict(
         source_type="ISSUE",
         data={
             "title": "[something] A title",
@@ -916,9 +763,8 @@ def test_github_tag_teams(tmpdir_factory):
             ),
         },
         check="No one to cc, exiting",
-    )
-
-    run(
+    ),
+    mismatching_tags=dict(
         source_type="PR",
         data={
             "title": "[something] A title",
@@ -936,9 +782,8 @@ def test_github_tag_teams(tmpdir_factory):
             ),
         },
         check="No one to cc, exiting",
-    )
-
-    run(
+    ),
+    draft_pr=dict(
         source_type="PR",
         data={
             "title": "[something] A title",
@@ -956,9 +801,8 @@ def test_github_tag_teams(tmpdir_factory):
             ),
         },
         check="Terminating since 1234 is a draft",
-    )
-
-    run(
+    ),
+    edit_inplace=dict(
         source_type="ISSUE",
         data={
             "title": "[something] A title",
@@ -974,9 +818,8 @@ def test_github_tag_teams(tmpdir_factory):
         check="would have updated issues/1234 with {'body': '`mold` and `lld` can be a much"
         " faster alternative to `ld` from gcc. We should modify our CMakeLists.txt to "
         "detect and use these when possible. cc @person1\\n\\ncc @person2 @person4'}",
-    )
-
-    run(
+    ),
+    edit_out_of_place=dict(
         source_type="ISSUE",
         data={
             "title": "[something3] A title",
@@ -989,9 +832,8 @@ def test_github_tag_teams(tmpdir_factory):
         },
         check="Dry run, would have updated issues/1234 with"
         " {'body': '@person2 @SOME1-ONE-\\n\\ncc @person1'}",
-    )
-
-    run(
+    ),
+    atted_but_not_cced=dict(
         source_type="ISSUE",
         data={
             "title": "[] A title",
@@ -1003,12 +845,64 @@ def test_github_tag_teams(tmpdir_factory):
             "body": "@person2 @SOME1-ONE-",
         },
         check="No one to cc, exiting",
+    ),
+)
+def test_github_tag_teams(tmpdir_factory, source_type, data, check):
+    """
+    Check that individuals are tagged from team headers
+    """
+    tag_script = REPO_ROOT / "ci" / "scripts" / "github_tag_teams.py"
+
+    git = TempGit(tmpdir_factory.mktemp("tmp_git_dir"))
+
+    issue_body = """
+    some text
+    [temporary] opt-in: @person5
+
+    - something: @person1 @person2
+    - something3: @person1 @person2 @SOME1-ONE-
+    - something else @person1 @person2
+    - something else2: @person1 @person2
+    - something-else @person1 @person2
+    """
+    comment1 = """
+    another thing: @person3
+    another-thing @person3
+    """
+    comment2 = """
+    something @person4
+    @person5
+    """
+    teams = {
+        "data": {
+            "repository": {
+                "issue": {
+                    "body": issue_body,
+                    "comments": {"nodes": [{"body": comment1}, {"body": comment2}]},
+                }
+            }
+        }
+    }
+    env = {
+        source_type: json.dumps(data),
+    }
+    proc = run_script(
+        [
+            tag_script,
+            "--dry-run",
+            "--team-issue-json",
+            json.dumps(teams),
+        ],
+        cwd=git.cwd,
+        env=env,
     )
 
+    assert_in(check, proc.stdout)
+
 
 @tvm.testing.skip_if_wheel_test
 @parameterize_named(
-    dict(
+    same_tags=dict(
         tlcpackstaging_body={
             "results": [
                 {
@@ -1028,7 +922,7 @@ def test_github_tag_teams(tmpdir_factory):
         expected="Tag names were the same, no update needed",
         expected_images=[],
     ),
-    dict(
+    staging_update=dict(
         tlcpackstaging_body={
             "results": [
                 {
@@ -1054,7 +948,7 @@ def test_github_tag_teams(tmpdir_factory):
             "ci_arm = 'tlcpack/ci-arm:456-456-abc'",
         ],
     ),
-    dict(
+    tlcpack_update=dict(
         tlcpackstaging_body={
             "results": [
                 {
@@ -1084,22 +978,19 @@ def test_open_docker_update_pr(
     tag_script = REPO_ROOT / "ci" / "scripts" / "open_docker_update_pr.py"
 
     git = TempGit(tmpdir_factory.mktemp("tmp_git_dir"))
-    git.run("init")
     git.run("config", "user.name", "ci")
     git.run("config", "user.email", "email@example.com")
-    git.run("checkout", "-b", "main")
-    git.run("remote", "add", "origin", "https://github.com/apache/tvm.git")
     images = [
-        "ci_lint",
-        "ci_gpu",
-        "ci_cpu",
-        "ci_minimal",
-        "ci_wasm",
-        "ci_i386",
-        "ci_cortexm",
         "ci_arm",
+        "ci_cortexm",
+        "ci_cpu",
+        "ci_gpu",
         "ci_hexagon",
+        "ci_i386",
+        "ci_lint",
+        "ci_minimal",
         "ci_riscv",
+        "ci_wasm",
     ]
 
     docker_data = {}
@@ -1107,52 +998,43 @@ def test_open_docker_update_pr(
         docker_data[f"repositories/tlcpackstaging/{image}/tags"] = tlcpackstaging_body
         docker_data[f"repositories/tlcpack/{image.replace('_', '-')}/tags"] = tlcpack_body
 
-    proc = subprocess.run(
+    proc = run_script(
         [
-            str(tag_script),
+            tag_script,
             "--dry-run",
             "--testing-docker-data",
             json.dumps(docker_data),
         ],
-        stdout=subprocess.PIPE,
-        stderr=subprocess.STDOUT,
-        encoding="utf-8",
         cwd=git.cwd,
         env={"GITHUB_TOKEN": "1234"},
-        check=False,
+        stderr=subprocess.STDOUT,
     )
 
     for line in expected_images:
         if line not in proc.stdout:
             raise RuntimeError(f"Missing line {line} in output:\n{proc.stdout}")
 
-    if proc.returncode != 0:
-        raise RuntimeError(f"Process failed:\nstdout:\n{proc.stdout}\n\nstderr:\n{proc.stderr}")
-
     assert_in(expected, proc.stdout)
 
 
-@pytest.mark.parametrize(
-    "images,expected",
-    [
-        (
-            ["ci_arm=tlcpack/ci-arm:abc-abc-123", "ci_lint=tlcpack/ci-lint:abc-abc-234"],
-            {
-                "ci_arm": "tlcpack/ci-arm:abc-abc-123",
-                "ci_lint": "tlcpack/ci-lint:abc-abc-234",
-            },
-        ),
-        (
-            ["ci_arm2=tlcpack/ci-arm2:abc-abc-123"],
-            {
-                "ci_arm2": "tlcpackstaging/ci_arm2:abc-abc-123",
-            },
-        ),
-    ],
+@parameterize_named(
+    use_tlcpack=dict(
+        images=["ci_arm=tlcpack/ci-arm:abc-abc-123", "ci_lint=tlcpack/ci-lint:abc-abc-234"],
+        expected={
+            "ci_arm": "tlcpack/ci-arm:abc-abc-123",
+            "ci_lint": "tlcpack/ci-lint:abc-abc-234",
+        },
+    ),
+    use_staging=dict(
+        images=["ci_arm2=tlcpack/ci-arm2:abc-abc-123"],
+        expected={
+            "ci_arm2": "tlcpackstaging/ci_arm2:abc-abc-123",
+        },
+    ),
 )
 def test_determine_docker_images(tmpdir_factory, images, expected):
     """Test script to decide whether to use tlcpack or tlcpackstaging for images"""
-    tag_script = REPO_ROOT / "ci" / "scripts" / "determine_docker_images.py"
+    script = REPO_ROOT / "ci" / "scripts" / "determine_docker_images.py"
 
     git_dir = tmpdir_factory.mktemp("tmp_git_dir")
 
@@ -1161,23 +1043,17 @@ def test_determine_docker_images(tmpdir_factory, images, expected):
         "repositories/tlcpack/ci-lint/tags/abc-abc-234": {},
     }
 
-    proc = subprocess.run(
+    run_script(
         [
-            str(tag_script),
+            script,
             "--testing-docker-data",
             json.dumps(docker_data),
             "--base-dir",
             git_dir,
         ]
         + images,
-        stdout=subprocess.PIPE,
-        stderr=subprocess.STDOUT,
-        encoding="utf-8",
         cwd=git_dir,
-        check=False,
     )
-    if proc.returncode != 0:
-        raise RuntimeError(f"Failed to run script:\n{proc.stdout}")
 
     for expected_filename, expected_image in expected.items():
         with open(Path(git_dir) / expected_filename) as f:
@@ -1186,34 +1062,28 @@ def test_determine_docker_images(tmpdir_factory, images, expected):
         assert actual_image == expected_image
 
 
-@pytest.mark.parametrize(
-    "changed_files,name,check,expected_code",
-    [
-        d.values()
-        for d in [
-            dict(
-                changed_files=[],
-                name="abc",
-                check="Image abc is not using new naming scheme",
-                expected_code=1,
-            ),
-            dict(
-                changed_files=[], name="123-123-abc", check="No extant hash found", expected_code=1
-            ),
-            dict(
-                changed_files=[["test.txt"]],
-                name=None,
-                check="Did not find changes, no rebuild necessary",
-                expected_code=0,
-            ),
-            dict(
-                changed_files=[["test.txt"], ["docker/test.txt"]],
-                name=None,
-                check="Found docker changes",
-                expected_code=2,
-            ),
-        ]
-    ],
+@parameterize_named(
+    invalid_name=dict(
+        changed_files=[],
+        name="abc",
+        check="Image abc is not using new naming scheme",
+        expected_code=1,
+    ),
+    no_hash=dict(
+        changed_files=[], name="123-123-abc", check="No extant hash found", expected_code=1
+    ),
+    no_changes=dict(
+        changed_files=[["test.txt"]],
+        name=None,
+        check="Did not find changes, no rebuild necessary",
+        expected_code=0,
+    ),
+    docker_changes=dict(
+        changed_files=[["test.txt"], ["docker/test.txt"]],
+        name=None,
+        check="Found docker changes",
+        expected_code=2,
+    ),
 )
 def test_should_rebuild_docker(tmpdir_factory, changed_files, name, check, expected_code):
     """
@@ -1222,11 +1092,8 @@ def test_should_rebuild_docker(tmpdir_factory, changed_files, name, check, expec
     tag_script = REPO_ROOT / "ci" / "scripts" / "should_rebuild_docker.py"
 
     git = TempGit(tmpdir_factory.mktemp("tmp_git_dir"))
-    git.run("init")
     git.run("config", "user.name", "ci")
     git.run("config", "user.email", "email@example.com")
-    git.run("checkout", "-b", "main")
-    git.run("remote", "add", "origin", "https://github.com/apache/tvm.git")
 
     git_path = Path(git.cwd)
     for i, commits in enumerate(changed_files):
@@ -1262,15 +1129,13 @@ def test_should_rebuild_docker(tmpdir_factory, changed_files, name, check, expec
         },
     }
 
-    proc = subprocess.run(
+    proc = run_script(
         [
-            str(tag_script),
+            tag_script,
             "--testing-docker-data",
             json.dumps(docker_data),
         ],
-        stdout=subprocess.PIPE,
         stderr=subprocess.STDOUT,
-        encoding="utf-8",
         cwd=git.cwd,
         check=False,
     )
diff --git a/tests/python/ci/test_tvmbot.py b/tests/python/ci/test_tvmbot.py
index 2c7a0eaec0..ceabd46a9b 100644
--- a/tests/python/ci/test_tvmbot.py
+++ b/tests/python/ci/test_tvmbot.py
@@ -18,13 +18,12 @@
 Test the @tvm-bot merge code
 """
 
-import subprocess
 import json
 from pathlib import Path
+from typing import Dict, Any
 
-import pytest
 import tvm
-from .test_utils import REPO_ROOT, TempGit
+from .test_utils import REPO_ROOT, TempGit, run_script
 
 
 SUCCESS_EXPECTED_OUTPUT = """
@@ -37,167 +36,244 @@ Dry run, would have merged with url=pulls/10786/merge and data={
 """.strip()
 
 
-TEST_DATA = {
-    "successful-merge": {
-        "number": 10786,
-        "filename": "pr10786-merges.json",
-        "expected": SUCCESS_EXPECTED_OUTPUT,
-        "comment": "@tvm-bot merge",
-        "user": "abc",
-        "detail": "Everything is fine so this PR will merge",
-    },
-    "no-request": {
-        "number": 10786,
-        "filename": "pr10786-nottriggered.json",
-        "expected": "Command 'do something else' did not match anything",
-        "comment": "@tvm-bot do something else",
-        "user": "abc",
-        "detail": "A PR for which the mergebot runs but no merge is requested",
-    },
-    "bad-ci": {
-        "number": 10786,
-        "filename": "pr10786-badci.json",
-        "expected": "Cannot merge, these CI jobs are not successful on",
-        "comment": "@tvm-bot merge",
-        "user": "abc",
-        "detail": "A PR which failed CI and cannot merge",
-    },
-    "old-review": {
-        "number": 10786,
-        "filename": "pr10786-oldreview.json",
-        "expected": "Cannot merge, did not find any approving reviews",
-        "comment": "@tvm-bot merge",
-        "user": "abc",
-        "detail": "A PR with passing CI and approving reviews on an old commit so it cannot merge",
-    },
-    "missing-job": {
-        "number": 10786,
-        "filename": "pr10786-missing-job.json",
-        "expected": "Cannot merge, missing expected jobs",
-        "comment": "@tvm-bot merge",
-        "user": "abc",
-        "detail": "PR missing an expected CI job and cannot merge",
-    },
-    "invalid-author": {
-        "number": 10786,
-        "filename": "pr10786-invalid-author.json",
-        "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": "Failed auth check 'collaborators'",
-        "comment": "@tvm-bot merge",
-        "user": "not-abc2",
-        "detail": "Check that a merge comment not from a CONTRIBUTOR is rejected",
-    },
-    "no-review": {
-        "number": 11267,
-        "filename": "pr11267-no-review.json",
-        "expected": "Cannot merge, did not find any approving reviews from users with write access",
-        "comment": "@tvm-bot merge",
-        "user": "abc",
-        "detail": "Check that a merge request without any reviews is rejected",
-    },
-    "changes-requested": {
-        "number": 10786,
-        "filename": "pr10786-changes-requested.json",
-        "expected": "Cannot merge, found [this review]",
-        "comment": "@tvm-bot merge",
-        "user": "abc",
-        "detail": "Check that a merge request with a 'Changes Requested' review is rejected",
-    },
-    "co-authors": {
-        "number": 10786,
-        "filename": "pr10786-co-authors.json",
-        "expected": "Co-authored-by: Some One <so...@email.com>",
-        "comment": "@tvm-bot merge",
-        "user": "abc",
-        "detail": "Check that a merge request with co-authors generates the correct commit message",
-    },
-    "rerun-ci": {
-        "number": 11442,
-        "filename": "pr11442-rerun-ci.json",
-        "expected": "Rerunning ci with",
-        "comment": "@tvm-bot rerun",
-        "user": "abc",
-        "detail": "Start a new CI job",
-    },
-    "ignore-jobs": {
-        "number": 10786,
-        "filename": "pr10786-ignore-jobs.json",
-        "expected": "Dry run, would have merged",
-        "comment": "@tvm-bot merge",
-        "user": "abc",
-        "detail": "Ignore GitHub Actions jobs that don't start with CI / ",
-    },
-}
+class _TvmBotTest:
+    NUMBER = 10786
+
+    def preprocess_data(self, data: Dict[str, Any]):
+        """
+        Used to pre-process PR data before running the test. Override as
+        necessary to edit data for specific test cases.
+        """
+        return data
+
+    @tvm.testing.skip_if_wheel_test
+    def test(self, tmpdir_factory):
+        """
+        Run the tvm-bot script using the data from preprocess_data
+        """
+        mergebot_script = REPO_ROOT / "ci" / "scripts" / "github_tvmbot.py"
+        test_json_dir = Path(__file__).resolve().parent / "sample_prs"
+        with open(test_json_dir / f"pr{self.NUMBER}.json") as f:
+            test_data = json.load(f)
+
+        # Update testing data with replacements / additions
+        test_data = self.preprocess_data(test_data)
+
+        git = TempGit(tmpdir_factory.mktemp("tmp_git_dir"))
+
+        comment = {
+            "body": self.COMMENT,
+            "id": 123,
+            "user": {
+                "login": self.USER,
+            },
+        }
+        allowed_users = [{"login": "abc"}, {"login": "other-abc"}]
+
+        proc = run_script(
+            [
+                mergebot_script,
+                "--pr",
+                self.NUMBER,
+                "--dry-run",
+                "--run-url",
+                "https://example.com",
+                "--testing-pr-json",
+                json.dumps(test_data),
+                "--testing-collaborators-json",
+                json.dumps(allowed_users),
+                "--testing-mentionable-users-json",
+                json.dumps(allowed_users),
+                "--trigger-comment-json",
+                json.dumps(comment),
+            ],
+            env={
+                "TVM_BOT_JENKINS_TOKEN": "123",
+                "GH_ACTIONS_TOKEN": "123",
+            },
+            cwd=git.cwd,
+        )
+
+        if self.EXPECTED not in proc.stderr:
+            raise RuntimeError(f"{proc.stderr}\ndid not contain\n{self.EXPECTED}")
+
+
+class TestNoRequest(_TvmBotTest):
+    """
+    A PR for which the mergebot runs but no merge is requested
+    """
+
+    COMMENT = "@tvm-bot do something else"
+    USER = "abc"
+    EXPECTED = "Command 'do something else' did not match anything"
+
+    def preprocess_data(self, data: Dict[str, Any]):
+        data["reviews"]["nodes"][0]["body"] = "nothing"
+        return data
+
+
+class TestSuccessfulMerge(_TvmBotTest):
+    """
+    Everything is fine so this PR will merge
+    """
+
+    COMMENT = "@tvm-bot merge"
+    USER = "abc"
+    EXPECTED = SUCCESS_EXPECTED_OUTPUT
+
+
+class TestBadCI(_TvmBotTest):
+    """
+    A PR which failed CI and cannot merge
+    """
+
+    COMMENT = "@tvm-bot merge"
+    USER = "abc"
+    EXPECTED = "Cannot merge, these CI jobs are not successful on"
+
+    def preprocess_data(self, data: Dict[str, Any]):
+        # Mark the Jenkins build as failed
+        contexts = data["commits"]["nodes"][0]["commit"]["statusCheckRollup"]["contexts"]["nodes"]
+        for context in contexts:
+            if "context" in context and context["context"] == "tvm-ci/pr-head":
+                context["state"] = "FAILED"
+        return data
+
+
+class TestOldReview(_TvmBotTest):
+    """
+    A PR with passing CI and approving reviews on an old commit so it cannot merge
+    """
+
+    COMMENT = "@tvm-bot merge"
+    USER = "abc"
+    EXPECTED = "Cannot merge, did not find any approving reviews"
+
+    def preprocess_data(self, data: Dict[str, Any]) -> Dict[str, Any]:
+        data["reviews"]["nodes"][0]["commit"]["oid"] = "abc12345"
+        return data
+
+
+class TestMissingJob(_TvmBotTest):
+    """
+    PR missing an expected CI job and cannot merge
+    """
+
+    COMMENT = "@tvm-bot merge"
+    USER = "abc"
+    EXPECTED = "Cannot merge, missing expected jobs"
+
+    def preprocess_data(self, data: Dict[str, Any]) -> Dict[str, Any]:
+        contexts = data["commits"]["nodes"][0]["commit"]["statusCheckRollup"]["contexts"]["nodes"]
+        for context in contexts:
+            if "context" in context and context["context"] == "tvm-ci/pr-head":
+                context["context"] = "something"
+        return data
+
+
+class TestInvalidAuthor(_TvmBotTest):
+    """
+    Merge requester is not a committer and cannot merge
+    """
+
+    COMMENT = "@tvm-bot merge"
+    USER = "not-abc"
+    EXPECTED = "Failed auth check 'collaborators', quitting"
 
 
-@tvm.testing.skip_if_wheel_test
-@pytest.mark.parametrize(
-    ["number", "filename", "expected", "comment", "user", "detail"],
-    [tuple(d.values()) for d in TEST_DATA.values()],
-    ids=TEST_DATA.keys(),
-)
-def test_tvmbot(tmpdir_factory, number, filename, expected, comment, user, detail):
-    """
-    Test the mergebot test cases
-    """
-    mergebot_script = REPO_ROOT / "ci" / "scripts" / "github_tvmbot.py"
-    test_json_dir = Path(__file__).resolve().parent / "sample_prs"
-
-    git = TempGit(tmpdir_factory.mktemp("tmp_git_dir"))
-    git.run("init", stderr=subprocess.PIPE, stdout=subprocess.PIPE)
-    git.run("checkout", "-b", "main", stderr=subprocess.PIPE, stdout=subprocess.PIPE)
-    git.run("remote", "add", "origin", "https://github.com/apache/tvm.git")
-    with open(test_json_dir / filename) as f:
-        test_data = json.load(f)
-
-    comment = {
-        "body": comment,
-        "id": 123,
-        "user": {
-            "login": user,
-        },
-    }
-    allowed_users = [{"login": "abc"}]
-
-    proc = subprocess.run(
-        [
-            str(mergebot_script),
-            "--pr",
-            str(number),
-            "--dry-run",
-            "--run-url",
-            "https://example.com",
-            "--testing-pr-json",
-            json.dumps(test_data),
-            "--testing-collaborators-json",
-            json.dumps(allowed_users),
-            "--testing-mentionable-users-json",
-            json.dumps(allowed_users),
-            "--trigger-comment-json",
-            json.dumps(comment),
-        ],
-        stdout=subprocess.PIPE,
-        stderr=subprocess.PIPE,
-        encoding="utf-8",
-        env={
-            "TVM_BOT_JENKINS_TOKEN": "123",
-            "GH_ACTIONS_TOKEN": "123",
-        },
-        cwd=git.cwd,
-        check=False,
-    )
-    if proc.returncode != 0:
-        raise RuntimeError(f"Process failed:\nstdout:\n{proc.stdout}\n\nstderr:\n{proc.stderr}")
-
-    if expected not in proc.stderr:
-        raise RuntimeError(f"{proc.stderr}\ndid not contain\n{expected}")
+class TestUnauthorizedComment(_TvmBotTest):
+    """
+    Check that a merge comment not from a CONTRIBUTOR is rejected
+    """
+
+    COMMENT = "@tvm-bot merge"
+    USER = "not-abc2"
+    EXPECTED = "Failed auth check 'collaborators'"
+
+
+class TestNoReview(_TvmBotTest):
+    """
+    Check that a merge request without any reviews is rejected
+    """
+
+    COMMENT = "@tvm-bot merge"
+    USER = "abc"
+    EXPECTED = "Cannot merge, did not find any approving reviews from users with write access"
+
+    def preprocess_data(self, data: Dict[str, Any]) -> Dict[str, Any]:
+        data["reviews"]["nodes"] = []
+        return data
+
+
+class TestChangesRequested(_TvmBotTest):
+    """
+    Check that a merge request with a 'Changes Requested' review is rejected
+    """
+
+    COMMENT = "@tvm-bot merge"
+    USER = "abc"
+    EXPECTED = "Cannot merge, found [this review]"
+
+    def preprocess_data(self, data: Dict[str, Any]) -> Dict[str, Any]:
+        data["reviews"]["nodes"][0]["state"] = "CHANGES_REQUESTED"
+        data["reviews"]["nodes"][0]["url"] = "http://example.com"
+        return data
+
+
+class TestCoAuthors(_TvmBotTest):
+    """
+    Check that a merge request with co-authors generates the correct commit message
+    """
+
+    COMMENT = "@tvm-bot merge"
+    USER = "abc"
+    EXPECTED = "Co-authored-by: Some One <so...@email.com>"
+
+    def preprocess_data(self, data: Dict[str, Any]) -> Dict[str, Any]:
+        data["authorCommits"]["nodes"][0]["commit"]["authors"]["nodes"].append(
+            {"name": "Some One", "email": "someone@email.com"}
+        )
+        return data
+
+
+class TestRerunCI(_TvmBotTest):
+    """
+    Start a new CI job
+    """
+
+    COMMENT = "@tvm-bot rerun"
+    USER = "abc"
+    EXPECTED = "Rerunning ci with"
+
+
+class TestRerunPermissions(_TvmBotTest):
+    """
+    Start a new CI job as an unauthorized user
+    """
+
+    COMMENT = "@tvm-bot rerun"
+    USER = "someone"
+    EXPECTED = "Failed auth check 'metionable_users', quitting"
+
+
+class TestRerunNonAuthor(_TvmBotTest):
+    """
+    Start a new CI job as a mentionable user
+    """
+
+    COMMENT = "@tvm-bot rerun"
+    USER = "other-abc"
+    EXPECTED = "Passed auth check 'metionable_users', continuing"
+
+
+class TestIgnoreJobs(_TvmBotTest):
+    """
+    Ignore GitHub Actions jobs that don't start with CI /
+    """
+
+    COMMENT = "@tvm-bot merge"
+    USER = "abc"
+    EXPECTED = "Dry run, would have merged"
 
 
 if __name__ == "__main__":
diff --git a/tests/python/ci/test_utils.py b/tests/python/ci/test_utils.py
index 513601aa1b..4a0f2710e7 100644
--- a/tests/python/ci/test_utils.py
+++ b/tests/python/ci/test_utils.py
@@ -19,19 +19,28 @@ Constants used in various CI tests
 """
 import subprocess
 import pathlib
+from typing import List, Any
 
 REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent.parent.parent
 
 
 class TempGit:
     """
-    A wrapper to run commands in a directory
+    A wrapper to run commands in a directory (specifically for use in CI tests)
     """
 
     def __init__(self, cwd):
         self.cwd = cwd
+        # Jenkins git is too old and doesn't have 'git init --initial-branch',
+        # so init and checkout need to be separate steps
+        self.run("init", stderr=subprocess.PIPE, stdout=subprocess.PIPE)
+        self.run("checkout", "-b", "main", stderr=subprocess.PIPE)
+        self.run("remote", "add", "origin", "https://github.com/apache/tvm.git")
 
     def run(self, *args, **kwargs):
+        """
+        Run a git command based on *args
+        """
         proc = subprocess.run(
             ["git"] + list(args), encoding="utf-8", cwd=self.cwd, check=False, **kwargs
         )
@@ -39,3 +48,25 @@ class TempGit:
             raise RuntimeError(f"git command failed: '{args}'")
 
         return proc
+
+
+def run_script(command: List[Any], check: bool = True, **kwargs):
+    """
+    Wrapper to run a script and print its output if there was an error
+    """
+    command = [str(c) for c in command]
+    kwargs_to_send = {
+        "stdout": subprocess.PIPE,
+        "stderr": subprocess.PIPE,
+        "encoding": "utf-8",
+    }
+    kwargs_to_send.update(kwargs)
+    proc = subprocess.run(
+        command,
+        check=False,
+        **kwargs_to_send,
+    )
+    if check and proc.returncode != 0:
+        raise RuntimeError(f"Process failed:\nstdout:\n{proc.stdout}\n\nstderr:\n{proc.stderr}")
+
+    return proc