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
4 changes: 3 additions & 1 deletion docs/node-maintenance.md
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,9 @@ fixes available** issue is created, updated, or reopened for the repository.
Availability does not prove compatibility or release-cooldown eligibility. Review
the upstream fix and everything your patch changes before upgrading, removing the
patch and obsolete assertion, and rerunning. **Patches are never removed automatically.**
A complete check with no remaining notifications closes the issue; unavailable
A complete check with no remaining notifications adds a resolution comment with
the verification run and closes the issue, preserving its last findings and any
maintainer notes. Later healthy runs do not add more comments. Unavailable
evidence does not close it. The reporter uses the existing `issues: write`
permission without checking out or executing project code.

Expand Down
3 changes: 3 additions & 0 deletions node/maintenance.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ optional maintainer comment, and verified official-fix availability in the
**Node local patches: official fixes available** issue when a newer stable fixed
release is verified. It never removes a patch or claims the release is compatible
with project constraints. Missing evidence cannot close that notification.
When a complete check clears the notification, the reporter preserves the issue
body, adds a resolution comment linking to the verification run, and closes it.
Further healthy runs leave the closed issue unchanged.

| Rerun result | Publisher behavior |
| --- | --- |
Expand Down
69 changes: 65 additions & 4 deletions node/runtime/configuration_issue.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Maintain one bot-owned configuration issue using a data-only audit report."""

import argparse
import hashlib
import html
import json
import os
Expand Down Expand Up @@ -172,10 +173,68 @@ def update_issue(repository, report, run_url, request=github_request):
)


def maintain_issue(repository, *, marker, title, body, failing, request=github_request):
def ensure_resolution_comment(repository, issue, body, request):
"""Reuse the bot's resolution for the same preserved finding body on retry."""
digest = hashlib.sha256(issue["body"].encode()).hexdigest()
marker = f"<!-- node-issue-resolution:v1:{digest} -->"
comment_body = marker + "\n\n" + body
if len(comment_body.encode()) > MAX_ISSUE_BYTES:
raise ValueError("issue resolution comment exceeds its size limit")
for page in range(1, 101):
comments = request(
"GET",
f"repos/{repository}/issues/{issue['number']}/comments?per_page=100&page={page}",
)
if not isinstance(comments, list) or any(
not isinstance(comment, dict) for comment in comments
):
raise ValueError("GitHub did not return valid issue comments")
for comment in comments:
author = comment.get("user", {})
if (
author.get("login") == BOT
and author.get("type") == "Bot"
and isinstance(comment.get("body"), str)
and comment["body"].startswith(marker + "\n")
):
return
if len(comments) < 100:
result = request(
"POST",
f"repos/{repository}/issues/{issue['number']}/comments",
{"body": comment_body},
)
if (
not isinstance(result, dict)
or type(result.get("id")) is not int
or result["id"] <= 0
):
raise ValueError("GitHub did not confirm the resolution comment")
return
raise ValueError(
"Cannot prove complete resolution comment history; issue left open"
)


def maintain_issue(
repository,
*,
marker,
title,
body,
failing,
request=github_request,
resolution_comment=None,
):
"""Reuse one bot-owned marked issue, including after closure."""
if not body.startswith(marker + "\n") or len(body.encode()) > MAX_ISSUE_BYTES:
raise ValueError("invalid marked issue body")
if resolution_comment is not None and (
not isinstance(resolution_comment, str)
or not resolution_comment.strip()
or len(resolution_comment.encode()) > MAX_ISSUE_BYTES
):
raise ValueError("invalid issue resolution comment")
issue = existing_issue(repository, request, marker)
if not failing and (issue is None or issue["state"] == "closed"):
return {"operation": "unchanged", "number": issue["number"] if issue else None}
Expand All @@ -188,10 +247,12 @@ def maintain_issue(repository, *, marker, title, body, failing, request=github_r
state = "open" if failing else "closed"
if issue["state"] == state and issue["body"] == body:
return {"operation": "unchanged", "number": issue["number"]}
payload = {"body": body, "state": state}
if not failing and resolution_comment is not None:
ensure_resolution_comment(repository, issue, resolution_comment, request)
payload = {"state": state}
result = request(
"PATCH",
f"repos/{repository}/issues/{issue['number']}",
{"body": body, "state": state},
"PATCH", f"repos/{repository}/issues/{issue['number']}", payload
)
operation = (
"closed"
Expand Down
11 changes: 11 additions & 0 deletions node/runtime/local_patch_issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,17 @@ def update_issue(repository, report, run_url, request=ISSUES["github_request"]):
body=body,
failing=available,
request=request,
resolution_comment=(
"Local-patch notification resolved.\n\n"
+ (
"No local patch declarations remain in the project configuration.\n\n"
if not report["patches"]
else "The latest complete audit has no declared local patch with a verified newer official fix requiring notification.\n\n"
+ preview
)
+ "The original findings above are preserved as history. This does not certify local patches or resolve unrelated security findings.\n\n"
+ f"[Verification run]({run_url})"
),
)


Expand Down
7 changes: 7 additions & 0 deletions node/tests/test_configuration_issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,23 @@ class GitHub:
def __init__(self, issues=None):
self.issues = copy.deepcopy(issues or [])
self.calls = []
self.comments = []

def request(self, method, path, payload=None):
self.calls.append((method, path, copy.deepcopy(payload)))
assert path.startswith("repos/owner/site/issues")
if method == "GET":
query = urllib.parse.parse_qs(urllib.parse.urlsplit(path).query)
page = int(query["page"][0])
if "/comments?" in path:
return copy.deepcopy(self.comments[(page - 1) * 100 : page * 100])
assert query["state"] == ["all"] and query["creator"] == [BOT["login"]]
return copy.deepcopy(self.issues[(page - 1) * 100 : page * 100])
if method == "POST":
if path.endswith("/comments"):
comment = {"id": len(self.comments) + 1, "user": BOT, **payload}
self.comments.append(comment)
return copy.deepcopy(comment)
issue = {"number": 77, "user": BOT, "state": "open", **payload}
self.issues.append(issue)
return copy.deepcopy(issue)
Expand Down
145 changes: 144 additions & 1 deletion node/tests/test_local_patches.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,13 +251,156 @@ def update(value, run=1):
)
assert update(value)["operation"] == "reopened"
assert len(github.issues) == 1
assert sum(method == "POST" for method, _, _ in github.calls) == 1
assert (
sum(
method == "POST" and not path.endswith("/comments")
for method, path, _ in github.calls
)
== 1
)
assert (
"&#124;" in github.issues[0]["body"]
and "<script>" not in github.issues[0]["body"]
)


def test_resolution_preserves_findings_and_manual_notes_then_comments_and_closes():
github = GitHub()
ISSUE["update_issue"]("owner/site", report(), "original-run", github.request)
issue = github.issues[0]
issue["body"] += "\n\nMaintainer note: check this patch's other changes."
original_body = issue["body"]
healthy = {"schema_version": 1, "complete": True, "patches": []}
result = ISSUE["update_issue"](
"owner/site", healthy, "verification-run", github.request
)
assert result == {"operation": "closed", "number": 77}
assert issue["state"] == "closed" and issue["body"] == original_body
assert ID in issue["body"] and "renderer@1.0.0" in issue["body"]
assert len(github.comments) == 1
comment = github.comments[0]["body"]
assert "No local patch declarations remain" in comment
assert "verification-run" in comment and "resolve unrelated" in comment
assert github.calls[-2][0:2] == ("POST", "repos/owner/site/issues/77/comments")
assert github.calls[-1] == (
"PATCH",
"repos/owner/site/issues/77",
{"state": "closed"},
)
assert (
ISSUE["update_issue"]("owner/site", healthy, "next-run", github.request)[
"operation"
]
== "unchanged"
)
assert len(github.comments) == 1 and issue["body"] == original_body


def test_failed_resolution_comment_keeps_issue_open_and_findings_unchanged():
github = GitHub()
ISSUE["update_issue"]("owner/site", report(), "run", github.request)
original = copy.deepcopy(github.issues[0])

def request(method, path, payload=None):
if method == "POST" and path.endswith("/comments"):
raise ValueError("GitHub comment request failed")
return github.request(method, path, payload)

with pytest.raises(ValueError, match="comment request failed"):
ISSUE["update_issue"](
"owner/site",
{"schema_version": 1, "complete": True, "patches": []},
"run",
request,
)
assert github.issues[0] == original


@pytest.mark.parametrize("earlier_comments", [0, 100])
def test_failed_close_reuses_resolution_comment_even_from_a_later_audit(
earlier_comments,
):
github = GitHub()
github.comments = [
{"id": i + 1, "user": {"login": "maintainer", "type": "User"}, "body": "Note"}
for i in range(earlier_comments)
]
ISSUE["update_issue"]("owner/site", report(), "run", github.request)
original_body = github.issues[0]["body"]
healthy = {"schema_version": 1, "complete": True, "patches": []}

def request(method, path, payload=None):
if method == "PATCH":
raise ValueError("Transient close failure")
return github.request(method, path, payload)

with pytest.raises(ValueError, match="close failure"):
ISSUE["update_issue"]("owner/site", healthy, "first-audit", request)
assert github.issues[0]["state"] == "open"
assert len(github.comments) == earlier_comments + 1
assert (
ISSUE["update_issue"]("owner/site", healthy, "later-audit", github.request)[
"operation"
]
== "closed"
)
assert len(github.comments) == earlier_comments + 1
assert "first-audit" in github.comments[-1]["body"]
assert github.issues[0]["body"] == original_body


def test_unavailable_comment_history_cannot_post_or_close():
github = GitHub()
ISSUE["update_issue"]("owner/site", report(), "run", github.request)
original = copy.deepcopy(github.issues[0])

def request(method, path, payload=None):
if "/comments?" in path:
return {"message": "Comments unavailable"}
return github.request(method, path, payload)

with pytest.raises(ValueError, match="valid issue comments"):
ISSUE["update_issue"](
"owner/site",
{"schema_version": 1, "complete": True, "patches": []},
"run",
request,
)
assert github.issues[0] == original and github.comments == []


def test_human_resolution_marker_cannot_suppress_bot_comment():
github = GitHub()
ISSUE["update_issue"]("owner/site", report(), "run", github.request)
healthy = {"schema_version": 1, "complete": True, "patches": []}
ISSUE["update_issue"]("owner/site", healthy, "audit", github.request)
github.comments[0]["user"] = {"login": "contributor", "type": "User"}
github.issues[0]["state"] = "open"
ISSUE["update_issue"]("owner/site", healthy, "retry", github.request)
assert len(github.comments) == 2 and "retry" in github.comments[-1]["body"]


def test_new_findings_get_their_own_resolution_comment():
github = GitHub()
healthy = {"schema_version": 1, "complete": True, "patches": []}
ISSUE["update_issue"]("owner/site", report(), "first-finding", github.request)
ISSUE["update_issue"]("owner/site", healthy, "first-resolution", github.request)
ISSUE["update_issue"]("owner/site", report(), "later-finding", github.request)
ISSUE["update_issue"]("owner/site", healthy, "later-resolution", github.request)
assert len(github.comments) == 2
assert "later-resolution" in github.comments[-1]["body"]


def test_resolution_with_remaining_assertions_explains_status_without_claiming_fixed():
github = GitHub()
ISSUE["update_issue"]("owner/site", report(), "run", github.request)
original_body = github.issues[0]["body"]
ISSUE["update_issue"]("owner/site", report("no-fix"), "new-run", github.request)
assert github.issues[0]["body"] == original_body
assert "No local patch declarations remain" not in github.comments[0]["body"]
assert ID in github.comments[0]["body"]


def test_unknown_official_fix_availability_cannot_close_an_issue():
data = evidence()
value = PATCHES["assess"]([ASSERTION], data, data, lambda *a, **kw: None, {})
Expand Down
Loading