Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .github/reviewer-bot-tests/test_reviewer_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,37 @@ def test_deferred_comment_missing_live_object_preserves_source_time_freshness(tm
assert state["active_reviews"]["42"]["deferred_gaps"]["issue_comment:99"]["reason"] == "reconcile_failed_closed"


def test_observer_noop_payload_is_safe_noop(tmp_path, monkeypatch):
state = make_state()
reviewer_bot.ensure_review_entry(state, 42, create=True)
payload_path = tmp_path / "observer-noop.json"
payload_path.write_text(
json.dumps(
{
"schema_version": 1,
"kind": "observer_noop",
"reason": "ignored_non_human_automation",
"source_workflow_name": "Reviewer Bot PR Comment Observer",
"source_workflow_file": ".github/workflows/reviewer-bot-pr-comment-observer.yml",
"source_run_id": 777,
"source_run_attempt": 1,
"source_event_name": "issue_comment",
"source_event_action": "created",
"source_event_key": "issue_comment:111",
"pr_number": 42,
}
),
encoding="utf-8",
)
monkeypatch.setenv("DEFERRED_CONTEXT_PATH", str(payload_path))
monkeypatch.setenv("WORKFLOW_RUN_TRIGGERING_NAME", "Reviewer Bot PR Comment Observer")
monkeypatch.setenv("WORKFLOW_RUN_TRIGGERING_ID", "777")
monkeypatch.setenv("WORKFLOW_RUN_TRIGGERING_ATTEMPT", "1")
monkeypatch.setenv("WORKFLOW_RUN_TRIGGERING_CONCLUSION", "success")
assert reviewer_bot.handle_workflow_run_event(state) is False
assert state["active_reviews"]["42"]["deferred_gaps"] == {}


def test_execute_pending_privileged_command_revalidates_live_state(monkeypatch):
state = make_state()
review = reviewer_bot.ensure_review_entry(state, 42, create=True)
Expand Down
86 changes: 51 additions & 35 deletions .github/workflows/reviewer-bot-pr-comment-observer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,43 +38,59 @@ jobs:
sender_type = os.environ.get('COMMENT_SENDER_TYPE', '').strip()
installation_id = os.environ.get('COMMENT_INSTALLATION_ID', '').strip()
via_github_app = os.environ.get('COMMENT_PERFORMED_VIA_GITHUB_APP', '').strip().lower()
noop_reason = None
if comment_user_type == 'Bot' or comment_author.endswith('[bot]') or comment_author == 'guidelines-bot':
raise SystemExit(0)
if installation_id or via_github_app == 'true' or (sender_type and sender_type not in {'User', 'Bot'}):
raise SystemExit(0)
command_pattern = re.compile(r'^@guidelines\-bot\s+/[A-Za-z0-9?_\-]+(?:\s+.*)?$')
lines = [line for line in normalized.splitlines() if line.strip()]
command_lines = [line for line in lines if command_pattern.match(line.strip())]
non_command_lines = [line for line in lines if not command_pattern.match(line.strip())]
if not normalized:
comment_class = 'empty_or_whitespace'
elif command_lines and not non_command_lines:
comment_class = 'command_only'
elif command_lines and non_command_lines:
comment_class = 'command_plus_text'
noop_reason = 'ignored_non_human_automation'
elif installation_id or via_github_app == 'true' or (sender_type and sender_type not in {'User', 'Bot'}):
noop_reason = 'ignored_non_human_automation'
if noop_reason is not None:
payload = {
'schema_version': 1,
'kind': 'observer_noop',
'reason': noop_reason,
'source_workflow_name': 'Reviewer Bot PR Comment Observer',
'source_workflow_file': '.github/workflows/reviewer-bot-pr-comment-observer.yml',
'source_run_id': int(os.environ['GITHUB_RUN_ID']),
'source_run_attempt': int(os.environ['GITHUB_RUN_ATTEMPT']),
'source_event_name': 'issue_comment',
'source_event_action': 'created',
'source_event_key': f"issue_comment:{os.environ['COMMENT_ID']}",
'pr_number': int(os.environ['PR_NUMBER']),
}
else:
comment_class = 'plain_text'
digest = hashlib.sha256(normalized.encode('utf-8')).hexdigest()
payload = {
'schema_version': 2,
'source_workflow_name': 'Reviewer Bot PR Comment Observer',
'source_workflow_file': '.github/workflows/reviewer-bot-pr-comment-observer.yml',
'source_run_id': int(os.environ['GITHUB_RUN_ID']),
'source_run_attempt': int(os.environ['GITHUB_RUN_ATTEMPT']),
'source_event_name': 'issue_comment',
'source_event_action': 'created',
'source_event_key': f"issue_comment:{os.environ['COMMENT_ID']}",
'pr_number': int(os.environ['PR_NUMBER']),
'comment_id': int(os.environ['COMMENT_ID']),
'comment_class': comment_class,
'has_non_command_text': bool(non_command_lines),
'source_body_digest': digest,
'source_created_at': os.environ['COMMENT_CREATED_AT'],
'actor_login': os.environ['COMMENT_AUTHOR'],
'actor_id': int(os.environ['COMMENT_AUTHOR_ID']),
'actor_class': 'repo_user_principal' if comment_user_type == 'User' else 'unknown_actor',
'source_artifact_name': f"reviewer-bot-comment-context-{os.environ['GITHUB_RUN_ID']}-attempt-{os.environ['GITHUB_RUN_ATTEMPT']}",
}
command_pattern = re.compile(r'^@guidelines\-bot\s+/[A-Za-z0-9?_\-]+(?:\s+.*)?$')
lines = [line for line in normalized.splitlines() if line.strip()]
command_lines = [line for line in lines if command_pattern.match(line.strip())]
non_command_lines = [line for line in lines if not command_pattern.match(line.strip())]
if not normalized:
comment_class = 'empty_or_whitespace'
elif command_lines and not non_command_lines:
comment_class = 'command_only'
elif command_lines and non_command_lines:
comment_class = 'command_plus_text'
else:
comment_class = 'plain_text'
digest = hashlib.sha256(normalized.encode('utf-8')).hexdigest()
payload = {
'schema_version': 2,
'source_workflow_name': 'Reviewer Bot PR Comment Observer',
'source_workflow_file': '.github/workflows/reviewer-bot-pr-comment-observer.yml',
'source_run_id': int(os.environ['GITHUB_RUN_ID']),
'source_run_attempt': int(os.environ['GITHUB_RUN_ATTEMPT']),
'source_event_name': 'issue_comment',
'source_event_action': 'created',
'source_event_key': f"issue_comment:{os.environ['COMMENT_ID']}",
'pr_number': int(os.environ['PR_NUMBER']),
'comment_id': int(os.environ['COMMENT_ID']),
'comment_class': comment_class,
'has_non_command_text': bool(non_command_lines),
'source_body_digest': digest,
'source_created_at': os.environ['COMMENT_CREATED_AT'],
'actor_login': os.environ['COMMENT_AUTHOR'],
'actor_id': int(os.environ['COMMENT_AUTHOR_ID']),
'actor_class': 'repo_user_principal' if comment_user_type == 'User' else 'unknown_actor',
'source_artifact_name': f"reviewer-bot-comment-context-{os.environ['GITHUB_RUN_ID']}-attempt-{os.environ['GITHUB_RUN_ATTEMPT']}",
}
with open(os.environ['PAYLOAD_PATH'], 'w', encoding='utf-8') as handle:
json.dump(payload, handle)
PY
Expand Down
36 changes: 36 additions & 0 deletions scripts/reviewer_bot_lib/reconcile.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,33 @@ def _load_deferred_context() -> dict:
return payload


def _validate_observer_noop_payload(payload: dict) -> None:
required = {
"schema_version",
"kind",
"reason",
"source_workflow_name",
"source_workflow_file",
"source_run_id",
"source_run_attempt",
"source_event_name",
"source_event_action",
"source_event_key",
"pr_number",
}
missing = sorted(required - set(payload))
if missing:
raise RuntimeError("Observer no-op payload missing required fields: " + ", ".join(missing))
if payload.get("schema_version") != 1:
raise RuntimeError("Observer no-op payload schema_version is not accepted")
if payload.get("kind") != "observer_noop":
raise RuntimeError("Observer no-op payload kind mismatch")
if not isinstance(payload.get("reason"), str) or not payload.get("reason"):
raise RuntimeError("Observer no-op payload reason must be a non-empty string")
if not isinstance(payload.get("pr_number"), int):
raise RuntimeError("Observer no-op payload pr_number must be an integer")


def _expected_observer_identity(payload: dict) -> tuple[str, str]:
event_name = payload.get("source_event_name")
event_action = payload.get("source_event_action")
Expand Down Expand Up @@ -291,6 +318,15 @@ def handle_workflow_run_event(bot, state: dict) -> bool:
event_action = payload.get("source_event_action")
source_event_key = str(payload.get("source_event_key", ""))
try:
if payload.get("kind") == "observer_noop":
_validate_observer_noop_payload(payload)
_validate_workflow_run_artifact_identity(payload)
print(
"Observer workflow produced explicit no-op payload for "
f"{source_event_key}: {payload.get('reason')}"
)
return False

if event_name == "issue_comment":
_validate_deferred_comment_artifact(payload)
_validate_workflow_run_artifact_identity(payload)
Expand Down
Loading