From 6b76724579ebf1c6c1dbb9dbff9cf9470d48fe4f Mon Sep 17 00:00:00 2001 From: Omry Yadan Date: Wed, 21 Oct 2026 02:53:00 +0800 Subject: [PATCH] Preserve configuration failure history when recovery closes its issue --- docs/node-maintenance.md | 2 +- node/runtime/configuration_issue.py | 7 ++- node/tests/test_configuration_issue.py | 67 +++++++++++++++++++++++++- 3 files changed, 72 insertions(+), 4 deletions(-) diff --git a/docs/node-maintenance.md b/docs/node-maintenance.md index 5c4ed7a..692d088 100644 --- a/docs/node-maintenance.md +++ b/docs/node-maintenance.md @@ -256,7 +256,7 @@ installation, validation, and the audit result: | Dependency changes with failed installation or checks | Retains a draft proposal when possible and fails the workflow. | | Incomplete or unavailable security audit | Fails the run and skips publication, even if project checks pass; retains the diagnostic summary and report artifact. | | No dependency changes | Creates no PR and leaves any existing proposal untouched; read the current run summary or report artifact. | -| Invalid configuration | Stops before dependency changes and updates one reusable configuration issue. Successful preflight closes it. | +| Invalid configuration | Stops before dependency changes and updates one reusable configuration issue. Successful preflight adds a verification comment and closes it, preserving the last failure description. | Choose **upgrades** for a separate manual run when you want routine updates. It also checks security and still obeys the cooldown and upgrade scope. It does diff --git a/node/runtime/configuration_issue.py b/node/runtime/configuration_issue.py index 8eb392a..4ef6abe 100644 --- a/node/runtime/configuration_issue.py +++ b/node/runtime/configuration_issue.py @@ -75,7 +75,7 @@ def display(value, limit): lines.extend( [ "", - "Align the named project declarations, or explicitly override the inherited settings in `.github/node-maintenance.json`, then rerun **Node dependency audit**. No project files were rewritten.", + "Align the named project declarations, or explicitly override the inherited settings in `.github/node-maintenance.json`, then rerun your dependency maintenance workflow. No project files were rewritten.", ] ) lines.extend( @@ -170,6 +170,11 @@ def update_issue(repository, report, run_url, request=github_request): body=issue_body(report, run_url), failing=report["status"] == "error", request=request, + resolution_comment=( + "Configuration preflight now passes.\n\n" + "The original failure description is preserved above.\n\n" + f"[Verification run]({run_url})" + ), ) diff --git a/node/tests/test_configuration_issue.py b/node/tests/test_configuration_issue.py index 83a4177..e68d2b2 100644 --- a/node/tests/test_configuration_issue.py +++ b/node/tests/test_configuration_issue.py @@ -80,14 +80,31 @@ def test_one_issue_across_failure_updates_resolution_and_recurrence(): "operation": "updated", "number": 77, } + failure_body = github.issues[0]["body"] assert update(github, "ok", 3) == {"operation": "closed", "number": 77} assert github.issues[0]["state"] == "closed" + assert github.issues[0]["body"] == failure_body + assert len(github.comments) == 1 + assert "Configuration preflight now passes." in github.comments[0]["body"] + assert "https://github.com/owner/site/actions/runs/3" in github.comments[0]["body"] assert update(github, "ok", 4) == {"operation": "unchanged", "number": 77} assert update(github, run=5) == {"operation": "reopened", "number": 77} assert github.issues[0]["state"] == "open" assert len(github.issues) == 1 - assert sum(method == "POST" for method, _, _ in github.calls) == 1 - assert all("/comments" not in path for _, path, _ in github.calls) + assert ( + sum( + method == "POST" and path == "repos/owner/site/issues" + for method, path, _ in github.calls + ) + == 1 + ) + assert ( + sum( + method == "POST" and path.endswith("/comments") + for method, path, _ in github.calls + ) + == 1 + ) def test_healthy_installation_does_not_create_an_issue_and_pending_does_not_close_one(): @@ -243,3 +260,49 @@ def test_reporting_job_is_serialized_and_has_no_consumer_checkout_or_app_key(): i for i, step in enumerate(steps) if step.get("id") == "proposal" ) assert upload_index < proposal_index + + +def test_configuration_recovery_keeps_failure_open_if_comment_cannot_be_written(): + github = GitHub() + update(github) + before = copy.deepcopy(github.issues) + + def request(method, path, payload=None): + if method == "POST" and path.endswith("/comments"): + raise ValueError("comment unavailable") + return github.request(method, path, payload) + + with pytest.raises(ValueError, match="comment unavailable"): + module["update_issue"]( + "owner/site", + report("ok"), + "https://github.com/owner/site/actions/runs/2", + request, + ) + assert github.issues == before + assert not any(method == "PATCH" for method, _, _ in github.calls) + + +def test_configuration_recovery_reuses_comment_after_close_request_failure(): + github = GitHub() + update(github) + failure_body = github.issues[0]["body"] + + def request(method, path, payload=None): + if method == "PATCH" and payload == {"state": "closed"}: + raise ValueError("close unavailable") + return github.request(method, path, payload) + + with pytest.raises(ValueError, match="close unavailable"): + module["update_issue"]( + "owner/site", + report("ok"), + "https://github.com/owner/site/actions/runs/2", + request, + ) + assert github.issues[0]["state"] == "open" + assert github.issues[0]["body"] == failure_body + assert len(github.comments) == 1 + assert update(github, "ok", 3) == {"operation": "closed", "number": 77} + assert github.issues[0]["body"] == failure_body + assert len(github.comments) == 1