[verified] Fix spec review source-Issue reference readback
This commit is contained in:
+124
-10
@@ -490,8 +490,10 @@ def _without(value, fields):
|
||||
return {key: child for key, child in value.items() if key not in fields}
|
||||
|
||||
|
||||
def _target_snapshot(snapshot, target):
|
||||
def _target_snapshot(snapshot, target, source_reference=False):
|
||||
fields = ("issue", "issue_comments", "pr", "pr_comments", "reviews", "timeline", "files")
|
||||
if source_reference:
|
||||
fields += ("issue_timeline", "pr_issue")
|
||||
_obj(snapshot, fields, "publication snapshot")
|
||||
issue, pr = snapshot["issue"], snapshot["pr"]
|
||||
if type(issue) is not dict or type(pr) is not dict:
|
||||
@@ -525,12 +527,12 @@ def _target_snapshot(snapshot, target):
|
||||
|
||||
def _projection_contract(contract):
|
||||
if contract is None:
|
||||
return False, False, None, False
|
||||
return False, False, None, False, False
|
||||
if type(contract) is not dict or type(contract.get("version")) is not dict or contract["version"].get("version") != "1.27.3":
|
||||
_fail("unsupported provider projection contract")
|
||||
flags = []
|
||||
for key in ("supersession_same_author_only", "already_dismissed_review_timestamp_projection",
|
||||
"requested_reviewers_presentation_order"):
|
||||
"requested_reviewers_presentation_order", "source_issue_cross_reference_projection"):
|
||||
value = contract.get(key, False)
|
||||
if type(value) is not bool:
|
||||
_fail("invalid provider projection contract")
|
||||
@@ -538,7 +540,114 @@ def _projection_contract(contract):
|
||||
official = contract.get("expected_official")
|
||||
if official is not None and type(official) is not bool:
|
||||
_fail("invalid provider review policy")
|
||||
return flags[0], flags[1], official, flags[2]
|
||||
return flags[0], flags[1], official, flags[2], flags[3]
|
||||
|
||||
|
||||
def collect_publication_snapshot(read_record, fetch_page, target, *, provider_contract=None,
|
||||
page_size=50, max_pages=10000):
|
||||
"""Read-only orchestration; callers own authentication and page wire envelopes."""
|
||||
_obj(target, TARGET, 'snapshot target')
|
||||
for key in ('owner', 'repository'):
|
||||
if not IDENT.fullmatch(_str(target[key], key, 1, 255)) or target[key] in ('.', '..'):
|
||||
_fail('invalid snapshot target')
|
||||
for key in ('issue', 'pull'):
|
||||
_positive_id(target[key])
|
||||
if not HEX40.fullmatch(_str(target['head'], 'head', 40, 40)):
|
||||
_fail('invalid snapshot head')
|
||||
source_reference = _projection_contract(provider_contract)[4]
|
||||
prefix = '/repos/' + target['owner'] + '/' + target['repository']
|
||||
issue = '/issues/' + str(target['issue'])
|
||||
pull = '/pulls/' + str(target['pull'])
|
||||
pr_issue = '/issues/' + str(target['pull'])
|
||||
out = {'issue': read_record(prefix + issue), 'pr': read_record(prefix + pull)}
|
||||
if source_reference:
|
||||
out['pr_issue'] = read_record(prefix + pr_issue)
|
||||
endpoints = [('issue_comments', issue + '/comments', 'id'),
|
||||
('pr_comments', pr_issue + '/comments', 'id'),
|
||||
('reviews', pull + '/reviews', 'id'),
|
||||
('timeline', pr_issue + '/timeline', 'id'),
|
||||
('files', pull + '/files', 'filename')]
|
||||
if source_reference:
|
||||
endpoints.append(('issue_timeline', issue + '/timeline', 'id'))
|
||||
for name, endpoint, key in endpoints:
|
||||
out[name] = collect_pages(lambda page, endpoint=endpoint: fetch_page(prefix + endpoint, page),
|
||||
key, page_size, max_pages)
|
||||
_target_snapshot(out, target, source_reference)
|
||||
return out
|
||||
|
||||
|
||||
def _source_issue_reference(before, after, event, target, pr_url, reviewer_id, causal_times):
|
||||
"""Admit only the source reference to this verified PR review event."""
|
||||
issue_url = pr_url.rsplit('/pulls/', 1)[0] + '/issues/' + str(target['issue'])
|
||||
fullname = target['owner'] + '/' + target['repository']
|
||||
for snapshot in (before, after):
|
||||
pr_issue = snapshot['pr_issue']
|
||||
if type(pr_issue) is not dict or type(pr_issue.get('repository')) is not dict:
|
||||
_fail('invalid PR Issue snapshot')
|
||||
_positive_id(pr_issue.get('id'))
|
||||
if (_positive_id(pr_issue.get('number')) != target['pull'] or
|
||||
pr_issue.get('html_url') != pr_url or
|
||||
pr_issue['repository'].get('full_name') != fullname or
|
||||
_user_id(pr_issue) != _user_id(snapshot['pr']) or
|
||||
pr_issue.get('state') != 'open' or
|
||||
type(pr_issue.get('pull_request')) is not dict or
|
||||
pr_issue['pull_request'].get('merged') is not False or
|
||||
pr_issue['pull_request'].get('html_url') != pr_url or
|
||||
snapshot['issue'].get('html_url') != issue_url):
|
||||
_fail('PR Issue target mismatch')
|
||||
_timestamp(pr_issue.get('updated_at'))
|
||||
old_pr_issue, pr_issue = before['pr_issue'], after['pr_issue']
|
||||
if (set(old_pr_issue) != set(pr_issue) or
|
||||
not _same(_without(old_pr_issue, {'updated_at', 'comments'}),
|
||||
_without(pr_issue, {'updated_at', 'comments'})) or
|
||||
pr_issue['updated_at'] not in causal_times):
|
||||
_fail('PR Issue drift')
|
||||
old_count, count = old_pr_issue.get('comments'), pr_issue.get('comments')
|
||||
if type(old_count) is not int or old_count < 0 or type(count) is not int or count != old_count + 1:
|
||||
_fail('PR Issue count projection mismatch')
|
||||
old_events = _index(before['issue_timeline'])
|
||||
events = _index(after['issue_timeline'])
|
||||
additions = set(events) - set(old_events)
|
||||
if (len(additions) != 1 or set(old_events) - set(events) or
|
||||
[item['id'] for item in after['issue_timeline'][:-1]] != list(old_events)):
|
||||
_fail('source timeline cardinality or order mismatch')
|
||||
# Existing ref_issue is a current Issue serializer, not an event-time snapshot.
|
||||
# Allow that nested projection only when it equals independent /issues/<PR> readbacks.
|
||||
for old, current in zip(before['issue_timeline'], after['issue_timeline'][:-1]):
|
||||
if _same(old, current):
|
||||
continue
|
||||
if (set(old) != set(current) or
|
||||
not _same(_without(old, {'ref_issue'}), _without(current, {'ref_issue'})) or
|
||||
not _same(old.get('ref_issue'), old_pr_issue) or
|
||||
not _same(current.get('ref_issue'), pr_issue)):
|
||||
_fail('previous source timeline drift')
|
||||
reference = after['issue_timeline'][-1]
|
||||
reference_id = _positive_id(reference.get('id'))
|
||||
if (reference_id not in additions or reference.get('type') != 'comment_ref' or
|
||||
_user_id(reference) != reviewer_id or
|
||||
not _same(reference.get('user'), event['user']) or
|
||||
reference.get('html_url') != issue_url + '#issuecomment-' + str(reference_id) or
|
||||
reference.get('issue_url') != issue_url or reference.get('pull_request_url') != '' or
|
||||
reference.get('body') != '' or reference.get('ref_action') != 'none' or
|
||||
type(reference.get('review_id')) is not int or reference['review_id'] != 0 or
|
||||
not _same(reference.get('ref_issue'), pr_issue)):
|
||||
_fail('source reference target mismatch')
|
||||
comment = reference.get('ref_comment')
|
||||
if type(comment) is not dict or _positive_id(comment.get('id')) != event['id']:
|
||||
_fail('source reference review event mismatch')
|
||||
for key in ('user', 'body', 'created_at', 'updated_at', 'pull_request_url'):
|
||||
if key not in comment or not _same(comment[key], event.get(key)):
|
||||
_fail('source reference review event mismatch')
|
||||
if (comment.get('html_url') != pr_url + '#issuecomment-' + str(event['id']) or
|
||||
comment.get('issue_url') != '' or not _same(comment.get('assets'), [])):
|
||||
_fail('source reference review event mismatch')
|
||||
created = _timestamp(reference.get('created_at'))
|
||||
updated = _timestamp(reference.get('updated_at'))
|
||||
from datetime import datetime
|
||||
if (created != updated or after['issue'].get('updated_at') != created or
|
||||
datetime.fromisoformat(created) < datetime.fromisoformat(event['created_at'])):
|
||||
_fail('source reference timestamp mismatch')
|
||||
return [reference_id], {created}
|
||||
|
||||
|
||||
def verify_publication(before, after, exact_review, result, allowed_paths, reviewer_id, inline_comments, provider_contract=None, *, expected_pr_url):
|
||||
@@ -549,12 +658,12 @@ def verify_publication(before, after, exact_review, result, allowed_paths, revie
|
||||
_fail("inline comments are not supported")
|
||||
target = result["target"]
|
||||
_str(expected_pr_url, "canonical PR URL", 1, 4000)
|
||||
url = _target_snapshot(before, target)
|
||||
if url != expected_pr_url or _target_snapshot(after, target) != expected_pr_url:
|
||||
supersede, retouch, expected_official, reviewer_presentation_order, source_reference = _projection_contract(provider_contract)
|
||||
url = _target_snapshot(before, target, source_reference)
|
||||
if url != expected_pr_url or _target_snapshot(after, target, source_reference) != expected_pr_url:
|
||||
_fail("snapshot PR URL mismatch")
|
||||
if _user_id(before["pr"]) == reviewer_id:
|
||||
_fail("reviewer is PR author")
|
||||
supersede, retouch, expected_official, reviewer_presentation_order = _projection_contract(provider_contract)
|
||||
old_reviews, reviews = _index(before["reviews"]), _index(after["reviews"])
|
||||
if type(exact_review) is not dict:
|
||||
_fail("invalid exact review")
|
||||
@@ -592,12 +701,14 @@ def verify_publication(before, after, exact_review, result, allowed_paths, revie
|
||||
event_updated = _timestamp(event.get("updated_at"))
|
||||
# Bind the event by exact record ID/author/body, not coincident wall clocks.
|
||||
causal_times = {submitted, created, event_updated}
|
||||
source_ids, source_times = _source_issue_reference(before, after, event, target, url, reviewer_id, causal_times) if source_reference else ([], set())
|
||||
for section in ("issue", "pr"):
|
||||
b, a = before[section], after[section]
|
||||
permitted = {"updated_at"} if section == "issue" else {"updated_at", "review_comments", "requested_reviewers"}
|
||||
if not _same(_without(b, permitted), _without(a, permitted)) or set(b) != set(a):
|
||||
_fail("source or PR drift")
|
||||
if not _same(b.get("updated_at"), a.get("updated_at")) and a.get("updated_at") not in causal_times:
|
||||
permitted_times = causal_times | source_times if section == "issue" else causal_times
|
||||
if not _same(b.get("updated_at"), a.get("updated_at")) and a.get("updated_at") not in permitted_times:
|
||||
_fail("timestamp projection mismatch")
|
||||
bpr, apr = before["pr"], after["pr"]
|
||||
old_count, count = bpr.get("review_comments"), apr.get("review_comments")
|
||||
@@ -639,8 +750,11 @@ def verify_publication(before, after, exact_review, result, allowed_paths, revie
|
||||
if not (superseded or retouched) or current.get("official") is not False or current.get("dismissed") is not True:
|
||||
_fail("previous review projection not authorized")
|
||||
projected.append(old_id)
|
||||
return {"review_id": rid, "state": expected, "review_records_added": 1,
|
||||
"timeline_events_added": 1, "projected_review_ids": projected, "read_only": True}
|
||||
out = {"review_id": rid, "state": expected, "review_records_added": 1,
|
||||
"timeline_events_added": 1, "projected_review_ids": projected, "read_only": True}
|
||||
if source_reference:
|
||||
out["source_issue_reference_ids"] = source_ids
|
||||
return out
|
||||
|
||||
def _classification_bytes(path):
|
||||
"""Bounded regular-file read; caller independently admits source provenance."""
|
||||
|
||||
Reference in New Issue
Block a user