This is an automated email from the ASF dual-hosted git repository.
shuke987 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new cace2bb01f1 [improvement](workflow) Accept qualified local review
receipts (#66959)
cace2bb01f1 is described below
commit cace2bb01f15ee6f8160eff17004052bb9aa18cc
Author: shuke <[email protected]>
AuthorDate: Mon Sep 7 10:58:38 2026 +0800
[improvement](workflow) Accept qualified local review receipts (#66959)
Problem Summary:
The hosted code-review pipeline can run out of review tokens even when a
committer has already completed the equivalent `doris-repo-review`
workflow locally. Accept a strictly formatted `doris-repo-review/v1`
PASS comment as an alternative source for the existing `code-review`
commit status.
The trusted default-branch workflow now:
- accepts only comments authored by users with effective `write` or
`admin` repository permission;
- requires the receipt commit to equal the live PR head;
- requires the reviewed base to equal the live base or be its ancestor
by no more than 48 hours of commit history;
- enforces the exact Opus 5, Fable 5, and GPT-5.6 Sol model allowlist at
`xhigh`, `max`, or `ultra` effort;
- requires `PASS`, convergence, one to three rounds, and zero
Blocker/Major findings;
- writes `code-review: success` only after every check passes.
The 48-hour base rule is evaluated when the comment is created or
edited. Runtime identity remains an auditable local declaration rather
than cryptographic proof, so the existing write-permission boundary and
later sampling remain part of the trust model.
---
.github/scripts/resolve_code_review_status.py | 123 +++++++++
.github/scripts/test_resolve_code_review_status.py | 119 +++++++++
.../scripts/test_validate_review_pass_comment.py | 290 +++++++++++++++++++++
.github/scripts/validate_review_pass_comment.py | 235 +++++++++++++++++
.github/workflows/code-review-aggregate-status.yml | 85 ++++++
.github/workflows/code-review-runner.yml | 40 ++-
.github/workflows/code-review-sync-result.yml | 252 +++++++++++++++---
7 files changed, 1103 insertions(+), 41 deletions(-)
diff --git a/.github/scripts/resolve_code_review_status.py
b/.github/scripts/resolve_code_review_status.py
new file mode 100644
index 00000000000..cc81807e34e
--- /dev/null
+++ b/.github/scripts/resolve_code_review_status.py
@@ -0,0 +1,123 @@
+#!/usr/bin/env python3
+"""Resolve the SHA-wide code-review status from PR/base-specific sources."""
+
+from __future__ import annotations
+
+import argparse
+import json
+import re
+from dataclasses import dataclass
+from pathlib import Path
+
+
+SOURCE_CONTEXT_RE = re.compile(
+
r"code-review/source/(?:automated|local|skip)/pr-(\d+)/base-([0-9a-fA-F]{40})"
+)
+SHA_RE = re.compile(r"[0-9a-fA-F]{40}")
+
+
+class ResolutionError(ValueError):
+ """The supplied GitHub API data cannot be resolved safely."""
+
+
+@dataclass(frozen=True)
+class Resolution:
+ state: str
+ description: str
+
+
+def resolve_status(
+ pulls: list[object], statuses: list[object], *, head_sha: str
+) -> Resolution:
+ if SHA_RE.fullmatch(head_sha) is None:
+ raise ResolutionError("head SHA is not a full SHA")
+
+ open_contexts: set[tuple[int, str]] = set()
+ for item in pulls:
+ if not isinstance(item, dict) or item.get("state") != "open":
+ continue
+ head = item.get("head")
+ base = item.get("base")
+ if not isinstance(head, dict) or not isinstance(base, dict):
+ raise ResolutionError("pull request is missing head or base
information")
+ pull_head = head.get("sha")
+ base_sha = base.get("sha")
+ number = item.get("number")
+ if not isinstance(pull_head, str) or not isinstance(base_sha, str):
+ raise ResolutionError("pull request head or base SHA is invalid")
+ if pull_head.casefold() != head_sha.casefold():
+ continue
+ if not isinstance(number, int) or SHA_RE.fullmatch(base_sha) is None:
+ raise ResolutionError("pull request number or base SHA is invalid")
+ open_contexts.add((number, base_sha.casefold()))
+
+ if not open_contexts:
+ return Resolution(
+ "pending", f"No open pull request currently uses {head_sha[:12]}."
+ )
+
+ latest_by_context: dict[str, dict[str, object]] = {}
+ for item in statuses:
+ if not isinstance(item, dict):
+ raise ResolutionError("commit status entry is invalid")
+ context = item.get("context")
+ status_id = item.get("id")
+ state = item.get("state")
+ if not isinstance(context, str) or not isinstance(status_id, int):
+ raise ResolutionError("commit status context or id is invalid")
+ if not isinstance(state, str):
+ raise ResolutionError("commit status state is invalid")
+ previous = latest_by_context.get(context)
+ if previous is None or status_id > int(previous["id"]):
+ latest_by_context[context] = item
+
+ approved_contexts: set[tuple[int, str]] = set()
+ for context, item in latest_by_context.items():
+ match = SOURCE_CONTEXT_RE.fullmatch(context)
+ if match is not None and item["state"] == "success":
+ approved_contexts.add((int(match.group(1)),
match.group(2).casefold()))
+
+ missing = sorted(open_contexts - approved_contexts)
+ if missing:
+ number, base_sha = missing[0]
+ remaining = len(missing) - 1
+ suffix = f" (+{remaining} more)" if remaining else ""
+ return Resolution(
+ "pending",
+ f"Awaiting code review for PR #{number} at
{base_sha[:12]}{suffix}.",
+ )
+
+ count = len(open_contexts)
+ noun = "context" if count == 1 else "contexts"
+ return Resolution(
+ "success",
+ f"Code review passed for {count} open PR {noun} on {head_sha[:12]}.",
+ )
+
+
+def load_list(path: Path, name: str) -> list[object]:
+ value = json.loads(path.read_text(encoding="utf-8"))
+ if not isinstance(value, list):
+ raise ResolutionError(f"{name} JSON must be an array")
+ return value
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--pulls-file", type=Path, required=True)
+ parser.add_argument("--statuses-file", type=Path, required=True)
+ parser.add_argument("--head-sha", required=True)
+ args = parser.parse_args()
+
+ resolution = resolve_status(
+ load_list(args.pulls_file, "pulls"),
+ load_list(args.statuses_file, "statuses"),
+ head_sha=args.head_sha,
+ )
+ print(f"state={resolution.state}")
+ print(f"description={resolution.description}")
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/.github/scripts/test_resolve_code_review_status.py
b/.github/scripts/test_resolve_code_review_status.py
new file mode 100644
index 00000000000..113e7cca1bf
--- /dev/null
+++ b/.github/scripts/test_resolve_code_review_status.py
@@ -0,0 +1,119 @@
+#!/usr/bin/env python3
+
+import unittest
+
+from resolve_code_review_status import ResolutionError, resolve_status
+
+
+HEAD_SHA = "a" * 40
+OTHER_HEAD_SHA = "d" * 40
+BASE_SHA = "b" * 40
+OTHER_BASE_SHA = "c" * 40
+
+
+def pull(number: int, *, head: str = HEAD_SHA, base: str = BASE_SHA, state:
str = "open") -> dict:
+ return {
+ "number": number,
+ "state": state,
+ "head": {"sha": head},
+ "base": {"sha": base},
+ }
+
+
+def status(
+ status_id: int,
+ *,
+ source: str = "local",
+ pr_number: int = 123,
+ base: str = BASE_SHA,
+ state: str = "success",
+) -> dict:
+ return {
+ "id": status_id,
+ "state": state,
+ "context": f"code-review/source/{source}/pr-{pr_number}/base-{base}",
+ }
+
+
+class ResolveCodeReviewStatusTest(unittest.TestCase):
+ def test_accepts_one_matching_source(self) -> None:
+ result = resolve_status([pull(123)], [status(1)], head_sha=HEAD_SHA)
+ self.assertEqual("success", result.state)
+
+ def test_requires_every_open_pr_with_the_same_head(self) -> None:
+ pulls = [pull(123), pull(124, base=OTHER_BASE_SHA)]
+ statuses = [status(1, pr_number=123)]
+ result = resolve_status(pulls, statuses, head_sha=HEAD_SHA)
+ self.assertEqual("pending", result.state)
+ self.assertIn("PR #124", result.description)
+
+ def test_accepts_different_sources_for_shared_head(self) -> None:
+ pulls = [pull(123), pull(124, base=OTHER_BASE_SHA)]
+ statuses = [
+ status(1, pr_number=123),
+ status(2, source="automated", pr_number=124, base=OTHER_BASE_SHA),
+ ]
+ result = resolve_status(pulls, statuses, head_sha=HEAD_SHA)
+ self.assertEqual("success", result.state)
+ self.assertIn("2 open PR contexts", result.description)
+
+ def test_shared_head_recovers_after_another_pr_moves(self) -> None:
+ statuses = [status(1, pr_number=123)]
+ before = resolve_status(
+ [pull(123), pull(124, base=OTHER_BASE_SHA)],
+ statuses,
+ head_sha=HEAD_SHA,
+ )
+ after = resolve_status(
+ [pull(123), pull(124, head=OTHER_HEAD_SHA, base=OTHER_BASE_SHA)],
+ statuses,
+ head_sha=HEAD_SHA,
+ )
+ self.assertEqual("pending", before.state)
+ self.assertEqual("success", after.state)
+
+ def test_does_not_reuse_a_source_after_base_changes(self) -> None:
+ result = resolve_status(
+ [pull(123, base=OTHER_BASE_SHA)], [status(1)], head_sha=HEAD_SHA
+ )
+ self.assertEqual("pending", result.state)
+
+ def test_ignores_closed_prs_and_prs_for_other_heads(self) -> None:
+ pulls = [
+ pull(123),
+ pull(124, state="closed"),
+ pull(125, head=OTHER_HEAD_SHA),
+ ]
+ result = resolve_status(pulls, [status(1)], head_sha=HEAD_SHA)
+ self.assertEqual("success", result.state)
+
+ def test_latest_state_wins_within_one_source_context(self) -> None:
+ statuses = [status(1), status(2, state="pending")]
+ result = resolve_status([pull(123)], statuses, head_sha=HEAD_SHA)
+ self.assertEqual("pending", result.state)
+
+ def test_another_success_source_can_satisfy_the_context(self) -> None:
+ statuses = [
+ status(1),
+ status(2, state="pending"),
+ status(3, source="skip"),
+ ]
+ result = resolve_status([pull(123)], statuses, head_sha=HEAD_SHA)
+ self.assertEqual("success", result.state)
+
+ def test_ignores_unscoped_legacy_code_review_success(self) -> None:
+ statuses = [{"id": 1, "state": "success", "context": "code-review"}]
+ result = resolve_status([pull(123)], statuses, head_sha=HEAD_SHA)
+ self.assertEqual("pending", result.state)
+
+ def test_returns_pending_without_an_open_pr(self) -> None:
+ result = resolve_status([], [], head_sha=HEAD_SHA)
+ self.assertEqual("pending", result.state)
+
+ def test_rejects_malformed_api_data(self) -> None:
+ with self.assertRaisesRegex(ResolutionError, "base SHA"):
+ resolve_status([pull(123, base="short")], [], head_sha=HEAD_SHA)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/.github/scripts/test_validate_review_pass_comment.py
b/.github/scripts/test_validate_review_pass_comment.py
new file mode 100644
index 00000000000..6a6c6f54cb4
--- /dev/null
+++ b/.github/scripts/test_validate_review_pass_comment.py
@@ -0,0 +1,290 @@
+#!/usr/bin/env python3
+
+import subprocess
+import sys
+import tempfile
+import unittest
+from pathlib import Path
+
+from validate_review_pass_comment import ValidationError, validate_comment
+
+
+HEAD_SHA = "a" * 40
+BASE_SHA = "b" * 40
+LIVE_BASE_SHA = "c" * 40
+VALIDATOR = Path(__file__).with_name("validate_review_pass_comment.py")
+
+
+def make_comment(**overrides: str) -> str:
+ fields = {
+ "schema": "doris-repo-review/v1",
+ "status": "PASS",
+ "pr": "apache/doris#123",
+ "commit": HEAD_SHA,
+ "base": BASE_SHA,
+ "reviewed_at": "2026-08-18T12:01+00:00",
+ "reviewer": "doris-committer",
+ "model": "gpt-5.6-sol",
+ "effort": "xhigh",
+ "findings": "{blocker: 0, major: 0, minor: 1, nit: 2}",
+ "rounds": "2",
+ "converged": "true",
+ }
+ fields.update(overrides)
+ yaml_body = "\n".join(f"{key}: {value}" for key, value in fields.items())
+ return f"""<!-- doris-repo-review:v1:begin -->
+### Local pipeline review — ✅ PASS
+
+```yaml
+{yaml_body}
+```
+
+**Notes for maintainers**
+
+_None._
+<!-- doris-repo-review:v1:end -->
+"""
+
+
+def validate(comment: str, **overrides: object) -> dict[str, object]:
+ arguments = {
+ "repository": "apache/doris",
+ "pr_number": 123,
+ "head_sha": HEAD_SHA,
+ "live_base_sha": LIVE_BASE_SHA,
+ "base_compare_status": "ahead",
+ "reviewed_base_committed_at": "2026-08-17T12:00:00Z",
+ "live_base_committed_at": "2026-08-19T11:59:00Z",
+ "comment_author": "doris-committer",
+ "comment_author_permission": "write",
+ }
+ arguments.update(overrides)
+ return validate_comment(
+ comment,
+ **arguments,
+ )
+
+
+class ValidateReviewPassCommentTest(unittest.TestCase):
+ def test_accepts_allowed_model_effort_combinations(self) -> None:
+ combinations = (
+ ("claude-opus-5", "xhigh"),
+ ("claude-opus-5", "max"),
+ ("claude-opus-5[1m]", "xhigh"),
+ ("claude-opus-5[1m]", "max"),
+ ("claude-fable-5", "xhigh"),
+ ("claude-fable-5", "max"),
+ ("claude-fable-5[1m]", "xhigh"),
+ ("claude-fable-5[1m]", "max"),
+ ("gpt-5.6-sol", "xhigh"),
+ ("gpt-5.6-sol", "max"),
+ ("gpt-5.6-sol", "ultra"),
+ )
+ for model, effort in combinations:
+ with self.subTest(model=model, effort=effort):
+ fields = validate(make_comment(model=model, effort=effort))
+ self.assertEqual(model, fields["model"])
+
+ def test_rejects_ultra_for_claude_models(self) -> None:
+ for model in (
+ "claude-opus-5",
+ "claude-opus-5[1m]",
+ "claude-fable-5",
+ "claude-fable-5[1m]",
+ ):
+ with self.subTest(model=model):
+ with self.assertRaisesRegex(ValidationError, "is not allowed
for model"):
+ validate(make_comment(model=model, effort="ultra"))
+
+ def test_rejects_effort_below_xhigh(self) -> None:
+ with self.assertRaisesRegex(ValidationError, "is not allowed for
model"):
+ validate(make_comment(effort="high"))
+
+ def test_rejects_unlisted_model(self) -> None:
+ with self.assertRaisesRegex(ValidationError, "model is not allowed"):
+ validate(make_comment(model="gpt-5.6"))
+
+ def test_rejects_a_different_head(self) -> None:
+ with self.assertRaisesRegex(ValidationError, "current PR head"):
+ validate(make_comment(commit="c" * 40))
+
+ def test_reviewed_at_does_not_expire_the_comment(self) -> None:
+ fields = validate(make_comment(reviewed_at="2020-01-01T00:00+00:00"))
+ self.assertEqual("2020-01-01T00:00+00:00", fields["reviewed_at"])
+
+ def test_accepts_the_current_base_regardless_of_commit_age(self) -> None:
+ fields = validate(
+ make_comment(base=LIVE_BASE_SHA),
+ base_compare_status="identical",
+ reviewed_base_committed_at=None,
+ live_base_committed_at=None,
+ )
+ self.assertEqual(LIVE_BASE_SHA, fields["base"])
+
+ def test_accepts_a_base_exactly_48_hours_behind(self) -> None:
+ fields = validate(
+ make_comment(),
+ reviewed_base_committed_at="2026-08-17T12:00:00Z",
+ live_base_committed_at="2026-08-19T12:00:00Z",
+ )
+ self.assertEqual(BASE_SHA, fields["base"])
+
+ def test_rejects_a_base_more_than_48_hours_behind(self) -> None:
+ with self.assertRaisesRegex(ValidationError, "more than 48 hours
behind"):
+ validate(
+ make_comment(),
+ reviewed_base_committed_at="2026-08-17T11:59:59Z",
+ live_base_committed_at="2026-08-19T12:00:00Z",
+ )
+
+ def test_rejects_a_diverged_base(self) -> None:
+ with self.assertRaisesRegex(ValidationError, "not an ancestor"):
+ validate(make_comment(), base_compare_status="diverged")
+
+ def test_rejects_inconsistent_equal_base_comparison(self) -> None:
+ with self.assertRaisesRegex(ValidationError, "compare status
identical"):
+ validate(make_comment(base=LIVE_BASE_SHA),
base_compare_status="ahead")
+
+ def test_rejects_base_commit_times_in_reverse_order(self) -> None:
+ with self.assertRaisesRegex(ValidationError, "after the current base"):
+ validate(
+ make_comment(),
+ reviewed_base_committed_at="2026-08-19T12:01:00Z",
+ live_base_committed_at="2026-08-19T12:00:00Z",
+ )
+
+ def test_rejects_missing_base_commit_times(self) -> None:
+ with self.assertRaisesRegex(ValidationError, "commit time is
required"):
+ validate(make_comment(), reviewed_base_committed_at=None)
+
+ def test_rejects_a_different_reviewer(self) -> None:
+ with self.assertRaisesRegex(ValidationError, "comment author"):
+ validate(make_comment(reviewer="someone-else"))
+
+ def test_accepts_admin_permission(self) -> None:
+ fields = validate(make_comment(), comment_author_permission="admin")
+ self.assertEqual("doris-committer", fields["reviewer"])
+
+ def test_rejects_non_write_permissions(self) -> None:
+ for permission in ("read", "triage", "none", ""):
+ with self.subTest(permission=permission):
+ with self.assertRaisesRegex(ValidationError, "write
permission"):
+ validate(make_comment(),
comment_author_permission=permission)
+
+ def test_rejects_a_different_pr(self) -> None:
+ with self.assertRaisesRegex(ValidationError, "different pull request"):
+ validate(make_comment(pr="apache/doris#124"))
+
+ def test_rejects_blocker_or_major_findings(self) -> None:
+ for findings in (
+ "{blocker: 1, major: 0, minor: 0, nit: 0}",
+ "{blocker: 0, major: 1, minor: 0, nit: 0}",
+ ):
+ with self.subTest(findings=findings):
+ with self.assertRaisesRegex(ValidationError, "must both be
zero"):
+ validate(make_comment(findings=findings))
+
+ def test_rejects_a_non_converged_review(self) -> None:
+ with self.assertRaisesRegex(ValidationError, "did not converge"):
+ validate(make_comment(converged="false"))
+
+ def test_rejects_rounds_outside_the_pipeline_limit(self) -> None:
+ for rounds in ("0", "4"):
+ with self.subTest(rounds=rounds):
+ with self.assertRaisesRegex(ValidationError, "between 1 and
3"):
+ validate(make_comment(rounds=rounds))
+
+ def test_rejects_duplicate_markers(self) -> None:
+ comment = make_comment() + make_comment()
+ with self.assertRaisesRegex(ValidationError, "exactly one v1 marker
pair"):
+ validate(comment)
+
+ def test_rejects_reversed_markers(self) -> None:
+ comment = make_comment()
+ marked_body = comment.split("<!-- doris-repo-review:v1:begin -->",
1)[1].split(
+ "<!-- doris-repo-review:v1:end -->", 1
+ )[0]
+ reversed_comment = (
+ "<!-- doris-repo-review:v1:end -->"
+ + marked_body
+ + "<!-- doris-repo-review:v1:begin -->"
+ )
+ with self.assertRaisesRegex(ValidationError, "out of order"):
+ validate(reversed_comment)
+
+ def test_extract_base_cli(self) -> None:
+ with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8") as
comment_file:
+ comment_file.write(make_comment())
+ comment_file.flush()
+ result = subprocess.run(
+ [
+ sys.executable,
+ str(VALIDATOR),
+ "extract-base",
+ "--comment-file",
+ comment_file.name,
+ ],
+ check=True,
+ capture_output=True,
+ text=True,
+ )
+ self.assertEqual(BASE_SHA, result.stdout.strip())
+
+ def test_extract_base_cli_rejects_a_non_sha(self) -> None:
+ with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8") as
comment_file:
+ comment_file.write(make_comment(base="not-a-sha"))
+ comment_file.flush()
+ result = subprocess.run(
+ [
+ sys.executable,
+ str(VALIDATOR),
+ "extract-base",
+ "--comment-file",
+ comment_file.name,
+ ],
+ check=False,
+ capture_output=True,
+ text=True,
+ )
+ self.assertEqual(1, result.returncode)
+ self.assertIn("base is not a full SHA", result.stderr)
+
+ def test_validate_cli_accepts_write_permission(self) -> None:
+ with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8") as
comment_file:
+ comment_file.write(make_comment())
+ comment_file.flush()
+ result = subprocess.run(
+ [
+ sys.executable,
+ str(VALIDATOR),
+ "validate",
+ "--comment-file",
+ comment_file.name,
+ "--repository",
+ "apache/doris",
+ "--pr-number",
+ "123",
+ "--head-sha",
+ HEAD_SHA,
+ "--live-base-sha",
+ LIVE_BASE_SHA,
+ "--base-compare-status",
+ "ahead",
+ "--reviewed-base-committed-at",
+ "2026-08-17T12:00:00Z",
+ "--live-base-committed-at",
+ "2026-08-19T11:59:00Z",
+ "--comment-author",
+ "doris-committer",
+ "--comment-author-permission",
+ "write",
+ ],
+ check=True,
+ capture_output=True,
+ text=True,
+ )
+ self.assertIn("VALID: local pipeline review passed", result.stdout)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/.github/scripts/validate_review_pass_comment.py
b/.github/scripts/validate_review_pass_comment.py
new file mode 100644
index 00000000000..5f33a8b5e95
--- /dev/null
+++ b/.github/scripts/validate_review_pass_comment.py
@@ -0,0 +1,235 @@
+#!/usr/bin/env python3
+"""Validate a doris-repo-review PASS comment for the current PR head."""
+
+from __future__ import annotations
+
+import argparse
+import datetime as dt
+import re
+import sys
+from pathlib import Path
+
+
+BEGIN_MARKER = "<!-- doris-repo-review:v1:begin -->"
+END_MARKER = "<!-- doris-repo-review:v1:end -->"
+SCHEMA = "doris-repo-review/v1"
+MAX_BASE_LAG = dt.timedelta(hours=48)
+
+ALLOWED_EFFORTS_BY_MODEL = {
+ "claude-opus-5": frozenset({"xhigh", "max"}),
+ "claude-opus-5[1m]": frozenset({"xhigh", "max"}),
+ "claude-fable-5": frozenset({"xhigh", "max"}),
+ "claude-fable-5[1m]": frozenset({"xhigh", "max"}),
+ "gpt-5.6-sol": frozenset({"xhigh", "max", "ultra"}),
+}
+ALLOWED_AUTHOR_PERMISSIONS = frozenset({"write", "admin"})
+EXPECTED_FIELDS = (
+ "schema",
+ "status",
+ "pr",
+ "commit",
+ "base",
+ "reviewed_at",
+ "reviewer",
+ "model",
+ "effort",
+ "findings",
+ "rounds",
+ "converged",
+)
+FINDINGS_RE = re.compile(
+ r"\{blocker: (\d+), major: (\d+), minor: (\d+), nit: (\d+)\}"
+)
+SHA_RE = re.compile(r"[0-9a-fA-F]{40}")
+
+
+class ValidationError(ValueError):
+ """The comment is not eligible to satisfy the code-review check."""
+
+
+def parse_comment(comment: str) -> dict[str, object]:
+ if comment.count(BEGIN_MARKER) != 1 or comment.count(END_MARKER) != 1:
+ raise ValidationError("expected exactly one v1 marker pair")
+ if comment.index(BEGIN_MARKER) > comment.index(END_MARKER):
+ raise ValidationError("v1 markers are out of order")
+
+ marked_body = comment.split(BEGIN_MARKER, 1)[1].split(END_MARKER, 1)[0]
+ yaml_blocks = re.findall(r"```yaml\n(.*?)\n```", marked_body,
flags=re.DOTALL)
+ if len(yaml_blocks) != 1:
+ raise ValidationError("expected exactly one fenced yaml block")
+
+ fields: dict[str, object] = {}
+ field_order: list[str] = []
+ for line in yaml_blocks[0].splitlines():
+ if ":" not in line:
+ raise ValidationError(f"malformed yaml field: {line!r}")
+ key, value = line.split(":", 1)
+ key = key.strip()
+ value = value.strip()
+ if key in fields:
+ raise ValidationError(f"duplicate field: {key}")
+ field_order.append(key)
+ fields[key] = value
+
+ if tuple(field_order) != EXPECTED_FIELDS:
+ raise ValidationError("comment fields do not match the v1 schema")
+
+ findings_match = FINDINGS_RE.fullmatch(str(fields["findings"]))
+ if findings_match is None:
+ raise ValidationError("findings must use the v1 inline-map format")
+ fields["findings"] = tuple(int(value) for value in findings_match.groups())
+
+ try:
+ fields["rounds"] = int(str(fields["rounds"]))
+ except ValueError as exc:
+ raise ValidationError("rounds must be an integer") from exc
+
+ return fields
+
+
+def validate_comment(
+ comment: str,
+ *,
+ repository: str,
+ pr_number: int,
+ head_sha: str,
+ live_base_sha: str,
+ base_compare_status: str,
+ reviewed_base_committed_at: str | None,
+ live_base_committed_at: str | None,
+ comment_author: str,
+ comment_author_permission: str,
+) -> dict[str, object]:
+ fields = parse_comment(comment)
+
+ if fields["schema"] != SCHEMA:
+ raise ValidationError(f"unsupported schema: {fields['schema']}")
+ if fields["status"] != "PASS":
+ raise ValidationError("status is not PASS")
+ if str(fields["pr"]).casefold() != f"{repository}#{pr_number}".casefold():
+ raise ValidationError("comment targets a different pull request")
+
+ reviewed_commit = str(fields["commit"])
+ require_full_sha("commit", reviewed_commit)
+ if reviewed_commit.casefold() != head_sha.casefold():
+ raise ValidationError("reviewed commit does not match the current PR
head")
+ reviewed_base_sha = str(fields["base"])
+ require_full_sha("base", reviewed_base_sha)
+ require_full_sha("live base", live_base_sha)
+
+ same_base = reviewed_base_sha.casefold() == live_base_sha.casefold()
+ if same_base:
+ if base_compare_status != "identical":
+ raise ValidationError("equal base SHAs must have compare status
identical")
+ else:
+ if base_compare_status != "ahead":
+ raise ValidationError("reviewed base is not an ancestor of the
current PR base")
+ reviewed_base_time = parse_timestamp(
+ "reviewed base commit time", reviewed_base_committed_at
+ )
+ live_base_time = parse_timestamp("current base commit time",
live_base_committed_at)
+ base_lag = live_base_time.astimezone(dt.timezone.utc) -
reviewed_base_time.astimezone(
+ dt.timezone.utc
+ )
+ if base_lag < dt.timedelta(0):
+ raise ValidationError("reviewed base commit time is after the
current base commit time")
+ if base_lag > MAX_BASE_LAG:
+ raise ValidationError("reviewed base is more than 48 hours behind
the current PR base")
+
+ if str(fields["reviewer"]).casefold() != comment_author.casefold():
+ raise ValidationError("reviewer does not match the GitHub comment
author")
+ if comment_author_permission not in ALLOWED_AUTHOR_PERMISSIONS:
+ raise ValidationError("comment author does not have write permission")
+ allowed_efforts = ALLOWED_EFFORTS_BY_MODEL.get(str(fields["model"]))
+ if allowed_efforts is None:
+ raise ValidationError(f"model is not allowed: {fields['model']}")
+ if fields["effort"] not in allowed_efforts:
+ raise ValidationError(
+ f"effort {fields['effort']} is not allowed for model
{fields['model']}"
+ )
+
+ blocker, major, _minor, _nit = fields["findings"]
+ if blocker != 0 or major != 0:
+ raise ValidationError("Blocker and Major findings must both be zero")
+ if fields["converged"] != "true":
+ raise ValidationError("review did not converge")
+ if not 1 <= int(fields["rounds"]) <= 3:
+ raise ValidationError("rounds must be between 1 and 3")
+
+ parse_timestamp("reviewed_at", str(fields["reviewed_at"]))
+
+ return fields
+
+
+def parse_timestamp(name: str, value: str | None) -> dt.datetime:
+ if value is None:
+ raise ValidationError(f"{name} is required")
+ try:
+ timestamp = dt.datetime.fromisoformat(value)
+ except ValueError as exc:
+ raise ValidationError(f"{name} is not a valid ISO-8601 timestamp")
from exc
+ if timestamp.tzinfo is None or timestamp.utcoffset() is None:
+ raise ValidationError(f"{name} must include a timezone offset")
+ return timestamp
+
+
+def require_full_sha(name: str, value: str) -> None:
+ if SHA_RE.fullmatch(value) is None:
+ raise ValidationError(f"{name} is not a full SHA")
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser()
+ subparsers = parser.add_subparsers(dest="command", required=True)
+
+ extract_parser = subparsers.add_parser("extract-base")
+ extract_parser.add_argument("--comment-file", type=Path, required=True)
+
+ validate_parser = subparsers.add_parser("validate")
+ validate_parser.add_argument("--comment-file", type=Path, required=True)
+ validate_parser.add_argument("--repository", required=True)
+ validate_parser.add_argument("--pr-number", type=int, required=True)
+ validate_parser.add_argument("--head-sha", required=True)
+ validate_parser.add_argument("--live-base-sha", required=True)
+ validate_parser.add_argument("--base-compare-status", required=True)
+ validate_parser.add_argument("--reviewed-base-committed-at")
+ validate_parser.add_argument("--live-base-committed-at")
+ validate_parser.add_argument("--comment-author", required=True)
+ validate_parser.add_argument("--comment-author-permission", required=True)
+ args = parser.parse_args()
+
+ try:
+ comment = args.comment_file.read_text(encoding="utf-8")
+ if args.command == "extract-base":
+ fields = parse_comment(comment)
+ if fields["schema"] != SCHEMA:
+ raise ValidationError(f"unsupported schema:
{fields['schema']}")
+ reviewed_base_sha = str(fields["base"])
+ require_full_sha("base", reviewed_base_sha)
+ print(reviewed_base_sha)
+ return 0
+ fields = validate_comment(
+ comment,
+ repository=args.repository,
+ pr_number=args.pr_number,
+ head_sha=args.head_sha,
+ live_base_sha=args.live_base_sha,
+ base_compare_status=args.base_compare_status,
+ reviewed_base_committed_at=args.reviewed_base_committed_at,
+ live_base_committed_at=args.live_base_committed_at,
+ comment_author=args.comment_author,
+ comment_author_permission=args.comment_author_permission,
+ )
+ except (OSError, ValidationError, ValueError) as exc:
+ print(f"INVALID: {exc}", file=sys.stderr)
+ return 1
+
+ print(
+ "VALID: local pipeline review passed "
+ f"with {fields['model']} at effort {fields['effort']} for
{fields['commit']}"
+ )
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/.github/workflows/code-review-aggregate-status.yml
b/.github/workflows/code-review-aggregate-status.yml
new file mode 100644
index 00000000000..89812d76e31
--- /dev/null
+++ b/.github/workflows/code-review-aggregate-status.yml
@@ -0,0 +1,85 @@
+name: Aggregate Code Review Status
+
+on:
+ workflow_call:
+ inputs:
+ head_sha:
+ description: Commit whose PR/base-specific review sources should be
aggregated.
+ required: true
+ type: string
+
+permissions: {}
+
+jobs:
+ aggregate:
+ runs-on: ubuntu-latest
+ concurrency:
+ group: code-review-status-${{ inputs.head_sha }}
+ cancel-in-progress: false
+ permissions:
+ contents: read
+ pull-requests: read
+ statuses: write
+ steps:
+ - name: Mark aggregate status as pending
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ HEAD_SHA: ${{ inputs.head_sha }}
+ REPO: ${{ github.repository }}
+ run: |
+ gh api "repos/${REPO}/statuses/${HEAD_SHA}" \
+ -X POST \
+ -f state='pending' \
+ -f context='code-review' \
+ -f description="Recalculating code review for ${HEAD_SHA:0:12}." \
+ -f target_url="${{ github.server_url }}/${{ github.repository
}}/actions/runs/${{ github.run_id }}"
+
+ - name: Checkout trusted status resolver
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ github.event.repository.default_branch }}
+ persist-credentials: false
+ sparse-checkout: .github/scripts/resolve_code_review_status.py
+ sparse-checkout-cone-mode: false
+
+ - name: Resolve review status across open PR contexts
+ id: resolution
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ HEAD_SHA: ${{ inputs.head_sha }}
+ REPO: ${{ github.repository }}
+ run: |
+ pull_pages="$RUNNER_TEMP/code-review-pull-pages.json"
+ pulls="$RUNNER_TEMP/code-review-pulls.json"
+ status_pages="$RUNNER_TEMP/code-review-status-pages.json"
+ statuses="$RUNNER_TEMP/code-review-statuses.json"
+
+ gh api --paginate --slurp \
+ -H 'Accept: application/vnd.github+json' \
+ "repos/${REPO}/pulls?state=open&per_page=100" > "$pull_pages"
+ jq '[.[][]]' "$pull_pages" > "$pulls"
+
+ gh api --paginate --slurp \
+ -H 'Accept: application/vnd.github+json' \
+ "repos/${REPO}/commits/${HEAD_SHA}/statuses?per_page=100" >
"$status_pages"
+ jq '[.[][]]' "$status_pages" > "$statuses"
+
+ python3 .github/scripts/resolve_code_review_status.py \
+ --pulls-file "$pulls" \
+ --statuses-file "$statuses" \
+ --head-sha "$HEAD_SHA" >> "$GITHUB_OUTPUT"
+
+ - name: Publish aggregate code-review status
+ env:
+ DESCRIPTION: ${{ steps.resolution.outputs.description }}
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ HEAD_SHA: ${{ inputs.head_sha }}
+ REPO: ${{ github.repository }}
+ STATE: ${{ steps.resolution.outputs.state }}
+ run: |
+ gh api "repos/${REPO}/statuses/${HEAD_SHA}" \
+ -X POST \
+ -f state="$STATE" \
+ -f context='code-review' \
+ -f description="$DESCRIPTION" \
+ -f target_url="${{ github.server_url }}/${{ github.repository
}}/actions/runs/${{ github.run_id }}"
diff --git a/.github/workflows/code-review-runner.yml
b/.github/workflows/code-review-runner.yml
index 6a0ed2e1b9a..4e8cbbdfb9c 100644
--- a/.github/workflows/code-review-runner.yml
+++ b/.github/workflows/code-review-runner.yml
@@ -55,6 +55,11 @@ permissions:
jobs:
code-review:
runs-on: ubuntu-latest
+ outputs:
+ base_sha: ${{ steps.review_inputs.outputs.base_sha }}
+ head_sha: ${{ steps.review_inputs.outputs.head_sha }}
+ manage_status: ${{ steps.review_inputs.outputs.manage_status }}
+ pr_number: ${{ steps.review_inputs.outputs.pr_number }}
# Pre-finalization steps can use 183 minutes and auth sync can use 8 more,
# leaving 12 minutes for runner setup and post-job cleanup.
timeout-minutes: 203
@@ -136,15 +141,25 @@ jobs:
if: ${{ steps.review_inputs.outputs.manage_status == 'true' }}
timeout-minutes: 2
env:
+ BASE_SHA: ${{ steps.review_inputs.outputs.base_sha }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HEAD_SHA: ${{ steps.review_inputs.outputs.head_sha }}
+ PR_NUMBER: ${{ steps.review_inputs.outputs.pr_number }}
REPO: ${{ github.repository }}
run: |
gh api "repos/${REPO}/statuses/${HEAD_SHA}" \
-X POST \
-f state='pending' \
-f context='code-review' \
- -f description="Automated review is running for ${HEAD_SHA}." \
+ -f description="Automated review is running for PR #${PR_NUMBER}."
\
+ -f target_url="${{ github.server_url }}/${{ github.repository
}}/actions/runs/${{ github.run_id }}"
+
+
source_context="code-review/source/automated/pr-${PR_NUMBER}/base-${BASE_SHA}"
+ gh api "repos/${REPO}/statuses/${HEAD_SHA}" \
+ -X POST \
+ -f state='pending' \
+ -f context="$source_context" \
+ -f description="Automated review is running for PR #${PR_NUMBER}
at ${BASE_SHA:0:12}." \
-f target_url="${{ github.server_url }}/${{ github.repository
}}/actions/runs/${{ github.run_id }}"
- name: Checkout repository
@@ -859,27 +874,30 @@ jobs:
}}
timeout-minutes: 2
env:
+ BASE_SHA: ${{ steps.review_inputs.outputs.base_sha }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HEAD_SHA: ${{ steps.review_inputs.outputs.head_sha }}
JOB_STATUS: ${{ job.status }}
+ PR_NUMBER: ${{ steps.review_inputs.outputs.pr_number }}
REPO: ${{ github.repository }}
REVIEW_CONTEXT_OUTCOME: ${{ steps.review_context.outcome }}
REVIEW_OUTCOME: ${{ steps.review.outcome }}
run: |
state="pending"
- summary="Trigger /review to start automated review for ${HEAD_SHA}."
+ summary="Automated review did not pass for PR #${PR_NUMBER} at
${BASE_SHA:0:12}."
if [ "$JOB_STATUS" = "success" ] && \
[ "$REVIEW_CONTEXT_OUTCOME" = "success" ] && \
[ "$REVIEW_OUTCOME" = "success" ]; then
state="success"
- summary="Automated review was triggered for ${HEAD_SHA}."
+ summary="Automated review passed for PR #${PR_NUMBER} at
${BASE_SHA:0:12}."
fi
+
source_context="code-review/source/automated/pr-${PR_NUMBER}/base-${BASE_SHA}"
gh api "repos/${REPO}/statuses/${HEAD_SHA}" \
-X POST \
-f state="${state}" \
- -f context='code-review' \
+ -f context="$source_context" \
-f description="${summary}" \
-f target_url="${{ github.server_url }}/${{ github.repository
}}/actions/runs/${{ github.run_id }}"
@@ -1012,3 +1030,17 @@ jobs:
OSS_AK: ${{ secrets.OSS_AK }}
OSS_SK: ${{ secrets.OSS_SK }}
OSS_ENDPOINT: oss-cn-hongkong.aliyuncs.com
+
+ aggregate-status:
+ needs: code-review
+ if: >-
+ always() &&
+ needs.code-review.outputs.manage_status == 'true' &&
+ needs.code-review.outputs.head_sha != ''
+ uses: ./.github/workflows/code-review-aggregate-status.yml
+ with:
+ head_sha: ${{ needs.code-review.outputs.head_sha }}
+ permissions:
+ contents: read
+ pull-requests: read
+ statuses: write
diff --git a/.github/workflows/code-review-sync-result.yml
b/.github/workflows/code-review-sync-result.yml
index ab84a4ab9c0..a45a9b06041 100644
--- a/.github/workflows/code-review-sync-result.yml
+++ b/.github/workflows/code-review-sync-result.yml
@@ -2,18 +2,183 @@ name: Code Review
on:
pull_request_target:
- types: [opened, synchronize, reopened, ready_for_review]
+ types: [opened, synchronize, reopened, ready_for_review, edited, closed]
issue_comment:
- types: [created, edited]
+ types: [created]
permissions:
+ contents: read
pull-requests: read
statuses: write
jobs:
+ accept-skill-review:
+ name: Accept local skill review PASS
+ runs-on: ubuntu-latest
+ outputs:
+ accepted: ${{ steps.validation.outputs.valid }}
+ head_sha: ${{ steps.validation.outputs.head_sha }}
+ if: >
+ github.event_name == 'issue_comment' &&
+ github.event.issue.pull_request != null &&
+ contains(github.event.comment.body, '<!-- doris-repo-review:v1:begin
-->')
+ steps:
+ # A local PASS is a creation-time credential. Later edits, deletion, or
+ # permission changes do not revoke it; edited events are intentionally
ignored.
+ - name: Authorize review comment creator
+ id: authorization
+ env:
+ COMMENT_AUTHOR: ${{ github.event.comment.user.login }}
+ EVENT_SENDER: ${{ github.event.sender.login }}
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ REPO: ${{ github.repository }}
+ run: |
+ if [ "$COMMENT_AUTHOR" != "$EVENT_SENDER" ]; then
+ echo "Review comment ignored: creator and event sender differ."
+ echo "authorized=false" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+
+ if ! permission_info="$(gh api
"repos/${REPO}/collaborators/${COMMENT_AUTHOR}/permission")"; then
+ echo "Review comment ignored: cannot verify ${COMMENT_AUTHOR}'s
repository permission."
+ echo "authorized=false" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+ if ! comment_author_permission="$(jq -er '.permission | strings'
<<<"$permission_info")"; then
+ echo "Review comment ignored: cannot read ${COMMENT_AUTHOR}'s
repository permission."
+ echo "authorized=false" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+ if [[ "$comment_author_permission" != "write" &&
"$comment_author_permission" != "admin" ]]; then
+ echo "Review comment ignored: ${COMMENT_AUTHOR} does not have
write permission."
+ echo "authorized=false" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+
+ echo "authorized=true" >> "$GITHUB_OUTPUT"
+ echo "permission=$comment_author_permission" >> "$GITHUB_OUTPUT"
+
+ - name: Checkout trusted validation script
+ if: steps.authorization.outputs.authorized == 'true'
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ github.event.repository.default_branch }}
+ persist-credentials: false
+ sparse-checkout: .github/scripts/validate_review_pass_comment.py
+ sparse-checkout-cone-mode: false
+
+ - name: Validate local review comment
+ if: steps.authorization.outputs.authorized == 'true'
+ id: validation
+ env:
+ COMMENT_AUTHOR_PERMISSION: ${{
steps.authorization.outputs.permission }}
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ REPO: ${{ github.repository }}
+ PR_NUMBER: ${{ github.event.issue.number }}
+ COMMENT_AUTHOR: ${{ github.event.comment.user.login }}
+ run: |
+ comment_file="$RUNNER_TEMP/doris-repo-review-comment.md"
+ jq -r '.comment.body' "$GITHUB_EVENT_PATH" > "$comment_file"
+
+ pr_info="$(gh api "repos/${REPO}/pulls/${PR_NUMBER}")"
+ head_sha="$(jq -r '.head.sha' <<<"$pr_info")"
+ live_base_sha="$(jq -r '.base.sha' <<<"$pr_info")"
+ pr_state="$(jq -r '.state' <<<"$pr_info")"
+ if [ "$pr_state" != "open" ]; then
+ echo "Review comment ignored: pull request is ${pr_state}."
+ echo "valid=false" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+
+ if ! reviewed_base_sha="$(python3
.github/scripts/validate_review_pass_comment.py \
+ extract-base --comment-file "$comment_file")"; then
+ echo "Review comment ignored: cannot read its reviewed base."
+ echo "valid=false" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+ if ! base_compare="$(gh api \
+
"repos/${REPO}/compare/${reviewed_base_sha}...${live_base_sha}")"; then
+ echo "Review comment ignored: cannot compare its base with the
current PR base."
+ echo "valid=false" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+ base_compare_status="$(jq -r '.status' <<<"$base_compare")"
+ if [ "$reviewed_base_sha" = "$live_base_sha" ]; then
+ reviewed_base_committed_at=""
+ live_base_committed_at=""
+ else
+ if ! reviewed_base_committed_at="$(gh api \
+ "repos/${REPO}/commits/${reviewed_base_sha}" --jq
'.commit.committer.date')" || \
+ ! live_base_committed_at="$(gh api \
+ "repos/${REPO}/commits/${live_base_sha}" --jq
'.commit.committer.date')"; then
+ echo "Review comment ignored: cannot resolve base commit times."
+ echo "valid=false" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+ fi
+
+ if validation_output="$(python3
.github/scripts/validate_review_pass_comment.py validate \
+ --comment-file "$comment_file" \
+ --repository "$REPO" \
+ --pr-number "$PR_NUMBER" \
+ --head-sha "$head_sha" \
+ --live-base-sha "$live_base_sha" \
+ --base-compare-status "$base_compare_status" \
+ --reviewed-base-committed-at "$reviewed_base_committed_at" \
+ --live-base-committed-at "$live_base_committed_at" \
+ --comment-author "$COMMENT_AUTHOR" \
+ --comment-author-permission "$COMMENT_AUTHOR_PERMISSION" 2>&1)";
then
+ echo "$validation_output"
+ echo "valid=true" >> "$GITHUB_OUTPUT"
+ echo "head_sha=$head_sha" >> "$GITHUB_OUTPUT"
+ echo "base_sha=$live_base_sha" >> "$GITHUB_OUTPUT"
+ else
+ echo "$validation_output"
+ echo "valid=false" >> "$GITHUB_OUTPUT"
+ fi
+
+ - name: Record local review source
+ if: steps.validation.outputs.valid == 'true'
+ env:
+ BASE_SHA: ${{ steps.validation.outputs.base_sha }}
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ REPO: ${{ github.repository }}
+ HEAD_SHA: ${{ steps.validation.outputs.head_sha }}
+ COMMENT_URL: ${{ github.event.comment.html_url }}
+ PR_NUMBER: ${{ github.event.issue.number }}
+ run: |
+
source_context="code-review/source/local/pr-${PR_NUMBER}/base-${BASE_SHA}"
+ gh api "repos/${REPO}/statuses/${HEAD_SHA}" \
+ -X POST \
+ -f state='pending' \
+ -f context='code-review' \
+ -f description="Recalculating code review for PR #${PR_NUMBER}." \
+ -f target_url="$COMMENT_URL"
+
+ gh api "repos/${REPO}/statuses/${HEAD_SHA}" \
+ -X POST \
+ -f state='success' \
+ -f context="$source_context" \
+ -f description="Local review passed for PR #${PR_NUMBER} at
${BASE_SHA:0:12}." \
+ -f target_url="$COMMENT_URL"
+
+ aggregate-after-skill-review:
+ needs: accept-skill-review
+ if: needs.accept-skill-review.outputs.accepted == 'true'
+ uses: ./.github/workflows/code-review-aggregate-status.yml
+ with:
+ head_sha: ${{ needs.accept-skill-review.outputs.head_sha }}
+ permissions:
+ contents: read
+ pull-requests: read
+ statuses: write
+
skip-on-comment:
name: Skip review via skip buildall comment
runs-on: ubuntu-latest
+ outputs:
+ accepted: ${{ steps.skip.outputs.accepted }}
+ head_sha: ${{ steps.skip.outputs.head_sha }}
if: >
github.event_name == 'issue_comment' &&
github.event.issue.pull_request != null &&
@@ -21,6 +186,7 @@ jobs:
contains(github.event.comment.body, 'skip buildall')
steps:
- name: Check user permission and mark review as success
+ id: skip
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
@@ -29,6 +195,7 @@ jobs:
run: |
PR_INFO=$(gh api repos/${REPO}/pulls/${PR_NUMBER})
HEAD_SHA=$(echo "${PR_INFO}" | jq -r '.head.sha')
+ BASE_SHA=$(echo "${PR_INFO}" | jq -r '.base.sha')
TARGET_BRANCH=$(echo "${PR_INFO}" | jq -r '.base.ref')
ALLOWED=false
@@ -48,51 +215,62 @@ jobs:
if [[ "${ALLOWED}" != 'true' ]]; then
echo "COMMENT_USER_ID ${COMMENT_USER_ID} is not allowed to skip
code review."
+ echo "accepted=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "COMMENT_USER_ID ${COMMENT_USER_ID} is allowed to skip code
review for ${TARGET_BRANCH}."
+
source_context="code-review/source/skip/pr-${PR_NUMBER}/base-${BASE_SHA}"
gh api repos/${REPO}/statuses/${HEAD_SHA} \
-X POST \
- -f state="success" \
+ -f state="pending" \
-f context='code-review' \
- -f description="Code review skipped via 'skip buildall' comment." \
+ -f description="Recalculating code review for PR #${PR_NUMBER}." \
-f target_url="${{ github.server_url }}/${{ github.repository
}}/actions/runs/${{ github.run_id }}"
+ gh api repos/${REPO}/statuses/${HEAD_SHA} \
+ -X POST \
+ -f state="success" \
+ -f context="$source_context" \
+ -f description="Code review skipped for PR #${PR_NUMBER} at
${BASE_SHA:0:12}." \
+ -f target_url="${{ github.server_url }}/${{ github.repository
}}/actions/runs/${{ github.run_id }}"
+
+ echo "accepted=true" >> "$GITHUB_OUTPUT"
+ echo "head_sha=$HEAD_SHA" >> "$GITHUB_OUTPUT"
+
+ aggregate-after-skip:
+ needs: skip-on-comment
+ if: needs.skip-on-comment.outputs.accepted == 'true'
+ uses: ./.github/workflows/code-review-aggregate-status.yml
+ with:
+ head_sha: ${{ needs.skip-on-comment.outputs.head_sha }}
+ permissions:
+ contents: read
+ pull-requests: read
+ statuses: write
+
sync-status:
name: Sync review status
- runs-on: ubuntu-latest
- timeout-minutes: 120
if: github.event_name == 'pull_request_target'
- steps:
- - name: Check automated review decision for current PR head
- env:
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- REPO: ${{ github.repository }}
- PR_NUMBER: ${{ github.event.pull_request.number }}
- HEAD_SHA: ${{ github.event.pull_request.head.sha }}
- run: |
- STATUSES=$(gh api --paginate
repos/${REPO}/commits/${HEAD_SHA}/status)
- review_state=$(printf '%s' "$STATUSES" | jq -r '
- ([ .statuses[]
- | select(.context == "code-review")
- ]
- | sort_by(.created_at)
- | last
- | .state) // ""
- ')
-
- state="pending"
- summary="Trigger /review to start automated review for ${HEAD_SHA}."
-
- if [ "$review_state" = "success" ]; then
- state="success"
- summary="Automated review was triggered for ${HEAD_SHA}."
- fi
+ uses: ./.github/workflows/code-review-aggregate-status.yml
+ with:
+ head_sha: ${{ github.event.pull_request.head.sha }}
+ permissions:
+ contents: read
+ pull-requests: read
+ statuses: write
- gh api repos/${REPO}/statuses/${HEAD_SHA} \
- -X POST \
- -f state="${state}" \
- -f context='code-review' \
- -f description="${summary}" \
- -f target_url="${{ github.server_url }}/${{ github.repository
}}/actions/runs/${{ github.run_id }}"
+ sync-previous-head-status:
+ name: Sync previous review status
+ if: >
+ github.event_name == 'pull_request_target' &&
+ github.event.action == 'synchronize' &&
+ github.event.before != '' &&
+ github.event.before != github.event.pull_request.head.sha
+ uses: ./.github/workflows/code-review-aggregate-status.yml
+ with:
+ head_sha: ${{ github.event.before }}
+ permissions:
+ contents: read
+ pull-requests: read
+ statuses: write
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]