mirror of
https://github.com/GreptimeTeam/greptimedb.git
synced 2026-10-03 18:45:35 +00:00
fix(ci): repair draft PR command dispatch (#9271)
* fix(ci): repair draft PR command dispatch Signed-off-by: WenyXu <wenymedia@gmail.com> * fix(ci): dispatch command workflows by branch ref Signed-off-by: WenyXu <wenymedia@gmail.com> * fix(ci): admit PR authors and writers for CI commands Signed-off-by: WenyXu <wenymedia@gmail.com> * fix(ci): preserve dispatch guard dependency semantics Signed-off-by: WenyXu <wenymedia@gmail.com> * fix(ci): configure slash command permissions individually Signed-off-by: WenyXu <wenymedia@gmail.com> * refactor(ci): run command workflows at dispatch branch head Signed-off-by: WenyXu <wenymedia@gmail.com> * fix(ci): rerun fork PR checks and report command failures Signed-off-by: WenyXu <wenymedia@gmail.com> --------- Signed-off-by: WenyXu <wenymedia@gmail.com>
This commit is contained in:
@@ -14,8 +14,11 @@
|
||||
# limitations under the License.
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import subprocess
|
||||
import textwrap
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
@@ -27,6 +30,9 @@ ci = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(ci)
|
||||
|
||||
|
||||
OPTIONS_TO_TEST = ["/ci", "/ci rust", "/ci fuzz chaos", "/ci fuzz all"]
|
||||
|
||||
|
||||
class CiSlashTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.output = tempfile.NamedTemporaryFile(delete=False)
|
||||
@@ -54,23 +60,145 @@ class CiSlashTest(unittest.TestCase):
|
||||
self.assertIn("skip=true", output)
|
||||
self.assertIn("Available draft-PR CI commands", output)
|
||||
|
||||
def test_dispatches_full_suite_at_verified_head(self):
|
||||
def test_dispatches_full_suite_with_top_level_admin_permission(self):
|
||||
output = self.run_main([
|
||||
{"body": "/ci", "issue_url": "https://api.github.test/repos/GreptimeTeam/greptimedb/issues/42", "user": {"login": "admin"}},
|
||||
{"state": "open", "draft": True, "head": {"sha": "a" * 40, "repo": {"full_name": "GreptimeTeam/greptimedb"}}},
|
||||
{"user": {"permission": "admin"}},
|
||||
{"state": "open", "draft": True, "head": {"sha": "a" * 40, "ref": "fix/draft-ci", "repo": {"full_name": "GreptimeTeam/greptimedb"}}},
|
||||
{"permission": "admin", "user": {"login": "admin"}},
|
||||
])
|
||||
self.assertIn("skip=false", output)
|
||||
self.assertIn("head_ref=fix/draft-ci", output)
|
||||
self.assertIn("workflow=rust.yml,integration.yml,checks.yml,docs.yml", output)
|
||||
|
||||
def test_rejects_non_draft_before_permission_lookup(self):
|
||||
output = self.run_main([
|
||||
{"body": "/ci fuzz chaos", "issue_url": "https://api.github.test/repos/GreptimeTeam/greptimedb/issues/42", "user": {"login": "admin"}},
|
||||
{"state": "open", "draft": False, "head": {"sha": "a" * 40, "repo": {"full_name": "GreptimeTeam/greptimedb"}}},
|
||||
{"state": "open", "draft": False, "head": {"sha": "a" * 40, "ref": "fix/draft-ci", "repo": {"full_name": "GreptimeTeam/greptimedb"}}},
|
||||
])
|
||||
self.assertIn("skip=true", output)
|
||||
self.assertIn("PR must be open and draft", output)
|
||||
|
||||
def test_rejects_missing_head_ref_before_permission_lookup(self):
|
||||
output = self.run_main([
|
||||
{"body": "/ci", "issue_url": "https://api.github.test/repos/GreptimeTeam/greptimedb/issues/42", "user": {"login": "admin"}},
|
||||
{"state": "open", "draft": True, "head": {"sha": "a" * 40, "repo": {"full_name": "GreptimeTeam/greptimedb"}}},
|
||||
])
|
||||
self.assertIn("skip=true", output)
|
||||
self.assertIn("PR head changed; comment again.", output)
|
||||
|
||||
def test_permission_matrix(self):
|
||||
for command in OPTIONS_TO_TEST:
|
||||
for author in (True, False):
|
||||
for permission in ("admin", "maintain", "write", "triage", "read", "none", None):
|
||||
with self.subTest(command=command, author=author, permission=permission):
|
||||
Path(self.output.name).write_text("")
|
||||
responses = [
|
||||
{"body": command, "issue_url": "https://api.github.test/repos/GreptimeTeam/greptimedb/issues/42", "user": {"login": "actor"}},
|
||||
{"state": "open", "draft": True, "user": {"login": "actor" if author else "other"}, "head": {"sha": "a" * 40, "ref": "fix/draft-ci", "repo": {"full_name": "GreptimeTeam/greptimedb"}}},
|
||||
]
|
||||
if command == "/ci fuzz all" or not author:
|
||||
responses.append({"permission": permission})
|
||||
output = self.run_main(responses)
|
||||
allowed = permission == "admin" if command == "/ci fuzz all" else author or permission in ("admin", "maintain", "write")
|
||||
self.assertIn("skip=false" if allowed else "skip=true", output)
|
||||
|
||||
def test_reply_uses_issue_comment_endpoint(self):
|
||||
workflow = SCRIPT.parents[1] / "workflows" / "ci-slash.yml"
|
||||
step = workflow.read_text().split(" - name: Reply with command result\n", 1)[1]
|
||||
command = textwrap.dedent(step.split(" run: |\n", 1)[1])
|
||||
reply = "Denied: `example`\nSecond line with $variables and 'quotes'"
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
gh = Path(directory) / "gh"
|
||||
gh.write_text("#!/usr/bin/env python3\nimport json, sys\nprint(json.dumps(sys.argv[1:]))\n")
|
||||
gh.chmod(0o755)
|
||||
env = {**os.environ, "PATH": directory + os.pathsep + os.environ["PATH"], "GITHUB_REPOSITORY": "GreptimeTeam/greptimedb", "PR_NUMBER": "42", "REPLY": reply, "ADMIT_OUTCOME": "success", "DISPATCH_OUTCOME": "success"}
|
||||
result = subprocess.run(["bash", "-eu", "-c", command], env=env, check=True, capture_output=True, text=True)
|
||||
self.assertEqual(json.loads(result.stdout), ["api", "--method", "POST", "/repos/GreptimeTeam/greptimedb/issues/42/comments", "-f", "body=" + reply])
|
||||
|
||||
def test_failure_reply_and_partial_dispatch(self):
|
||||
workflow = (SCRIPT.parents[1] / "workflows" / "ci-slash.yml").read_text()
|
||||
dispatch = textwrap.dedent(workflow.split(" - name: Dispatch selected CI workflow\n", 1)[1].split(" run: |\n", 1)[1].split(" - name: Reply", 1)[0])
|
||||
reply = textwrap.dedent(workflow.split(" - name: Reply with command result\n", 1)[1].split(" run: |\n", 1)[1])
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
gh = Path(directory) / "gh"
|
||||
log = Path(directory) / "calls"
|
||||
gh.write_text("#!/usr/bin/env python3\nimport json, os, sys\nwith open(os.environ['CALLS'], 'a') as f: f.write(json.dumps(sys.argv[1:]) + '\\n')\nsys.exit(1 if '/runs/8/rerun' in sys.argv[-1] else 0)\n")
|
||||
gh.chmod(0o755)
|
||||
env = {**os.environ, "PATH": directory + os.pathsep + os.environ["PATH"], "CALLS": str(log), "GITHUB_REPOSITORY": "GreptimeTeam/greptimedb", "RUN_IDS": "7,8,9", "PR_NUMBER": "42", "REPLY": "Success", "ADMIT_OUTCOME": "success", "DISPATCH_OUTCOME": "failure", "GITHUB_SERVER_URL": "https://github.com", "GITHUB_RUN_ID": "123"}
|
||||
result = subprocess.run(["bash", "-eu", "-c", dispatch], env=env, capture_output=True)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
subprocess.run(["bash", "-eu", "-c", reply], env=env, check=True, capture_output=True)
|
||||
calls = [json.loads(line) for line in log.read_text().splitlines()]
|
||||
self.assertEqual([call[-1] for call in calls[:2]], ["/repos/GreptimeTeam/greptimedb/actions/runs/7/rerun", "/repos/GreptimeTeam/greptimedb/actions/runs/8/rerun"])
|
||||
self.assertIn("CI trigger failed; some workflows may already have been requested", calls[2][-1])
|
||||
self.assertIn("/actions/runs/123", calls[2][-1])
|
||||
self.assertIn("!cancelled()", workflow.split(" - name: Reply with command result", 1)[1])
|
||||
|
||||
def test_fork_rerun_gates_cover_standard_jobs(self):
|
||||
import re
|
||||
for name, count in [("rust", 7), ("integration", 5), ("checks", 5), ("docs", 3)]:
|
||||
workflow = (SCRIPT.parents[1] / "workflows" / (name + ".yml")).read_text()
|
||||
gates = re.findall(r"^ if:.*github.run_attempt > 1.*$", workflow, re.MULTILINE)
|
||||
self.assertEqual(len(gates), count, name)
|
||||
self.assertTrue(all("always()" not in gate for gate in gates))
|
||||
|
||||
def test_command_permissions(self):
|
||||
workflow = SCRIPT.parents[1] / "workflows" / "slash-command-dispatch.yml"
|
||||
config = workflow.read_text().split("config: >-", 1)[1].split(" - name:", 1)[0]
|
||||
self.assertEqual(json.loads(config), [
|
||||
{"command": "query-regression", "permission": "admin", "issue_type": "pull-request"},
|
||||
{"command": "ci", "permission": "none", "issue_type": "pull-request"},
|
||||
])
|
||||
|
||||
def test_fork_requires_writer_even_for_author(self):
|
||||
for permission in ("read", "write", "maintain", "admin"):
|
||||
with self.subTest(permission=permission):
|
||||
Path(self.output.name).write_text("")
|
||||
responses = [
|
||||
{"body": "/ci", "issue_url": "https://api.github.test/issues/42", "user": {"login": "actor"}},
|
||||
{"state": "open", "draft": True, "user": {"login": "actor"}, "head": {"sha": "a" * 40, "ref": "fork-branch", "repo": {"full_name": "actor/greptimedb"}}},
|
||||
{"permission": permission},
|
||||
]
|
||||
if permission != "read":
|
||||
responses.append({"workflow_runs": [
|
||||
{"id": 7, "path": ".github/workflows/rust.yml", "head_sha": "a" * 40, "head_branch": "fork-branch", "head_repository": {"full_name": "actor/greptimedb"}, "status": "completed", "conclusion": "skipped"},
|
||||
{"id": 8, "path": ".github/workflows/checks.yml", "head_sha": "a" * 40, "head_branch": "fork-branch", "head_repository": {"full_name": "someone/greptimedb"}, "status": "completed", "conclusion": "skipped"},
|
||||
]})
|
||||
output = self.run_main(responses)
|
||||
if permission == "read":
|
||||
self.assertIn("skip=true", output)
|
||||
else:
|
||||
self.assertIn("run_ids=7\n", output)
|
||||
self.assertNotIn("head_ref=", output)
|
||||
|
||||
def test_fork_rejects_non_draft_original_run(self):
|
||||
output = self.run_main([
|
||||
{"body": "/ci rust", "issue_url": "https://api.github.test/issues/42", "user": {"login": "writer"}},
|
||||
{"state": "open", "draft": True, "head": {"sha": "a" * 40, "ref": "fork-branch", "repo": {"full_name": "actor/greptimedb"}}},
|
||||
{"permission": "write"},
|
||||
{"workflow_runs": [{"id": 7, "path": ".github/workflows/rust.yml@main", "head_sha": "a" * 40, "head_branch": "fork-branch", "head_repository": {"full_name": "actor/greptimedb"}, "status": "completed", "conclusion": "success", "run_attempt": 2}]},
|
||||
{"conclusion": "success"},
|
||||
])
|
||||
self.assertIn("only originally skipped draft CI runs", output)
|
||||
self.assertIn("skip=true", output)
|
||||
|
||||
def test_author_cannot_bypass_pr_guards(self):
|
||||
for change, reason in [
|
||||
({"state": "closed"}, "PR must be open and draft"),
|
||||
({"draft": False}, "PR must be open and draft"),
|
||||
({"head": {"sha": "b" * 40, "ref": "fix/draft-ci", "repo": {"full_name": "GreptimeTeam/greptimedb"}}}, "PR head changed"),
|
||||
]:
|
||||
with self.subTest(change=change):
|
||||
Path(self.output.name).write_text("")
|
||||
pr = {"state": "open", "draft": True, "user": {"login": "actor"}, "head": {"sha": "a" * 40, "ref": "fix/draft-ci", "repo": {"full_name": "GreptimeTeam/greptimedb"}}}
|
||||
pr.update(change)
|
||||
output = self.run_main([
|
||||
{"body": "/ci rust", "issue_url": "https://api.github.test/repos/GreptimeTeam/greptimedb/issues/42", "user": {"login": "actor"}},
|
||||
pr,
|
||||
])
|
||||
self.assertIn("skip=true", output)
|
||||
self.assertIn(reason, output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user