mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
fix(release): expose reconciliation decisions in run summaries
The successful scheduled reconciliation at run 33972475922 retained a credential-containment hold and dispatched no retry. Put safe decision messages in the Actions summary so a green reconciler is not mistaken for delivered releases. Narrow empty-discovery wording because mutable channels and missing runs are not qualified. Preserve containment, retry budgets and dispatch behaviour; cover summary output and empty discovery with focused tests. Change-source: pulse-maintainer
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import html
|
||||
from datetime import datetime
|
||||
import json
|
||||
import os
|
||||
@@ -478,6 +479,20 @@ def flatten_pages(pages: Iterable[object], key: str | None = None) -> list[objec
|
||||
return values
|
||||
|
||||
|
||||
def report_decision(message: str) -> None:
|
||||
"""Expose safe decision text, not private API responses, in the run summary."""
|
||||
print(message)
|
||||
summary = os.environ.get("GITHUB_STEP_SUMMARY")
|
||||
if summary:
|
||||
with open(summary, "a", encoding="utf-8") as output:
|
||||
output.write(
|
||||
"### Release convergence reconciliation\n\n"
|
||||
"A successful reconciliation job is not evidence of successful "
|
||||
"release convergence or installed customer health.\n\n"
|
||||
f"<pre>{html.escape(message)}</pre>\n\n"
|
||||
)
|
||||
|
||||
|
||||
def reconcile(github: GitHub, run_id: int, max_attempts: int) -> None:
|
||||
repository = github.repository
|
||||
repository_state = github.api(f"repos/{repository}")
|
||||
@@ -499,7 +514,7 @@ def reconcile(github: GitHub, run_id: int, max_attempts: int) -> None:
|
||||
path=CONVERGENCE_PATH,
|
||||
)
|
||||
if run.get("status") != "completed" or run.get("conclusion") not in TERMINAL_FAILURES:
|
||||
print(f"Convergence run {run_id} no longer has terminal convergence debt; no action.")
|
||||
report_decision(f"Convergence run {run_id} no longer has terminal convergence debt; no action.")
|
||||
return
|
||||
title = run.get("display_title")
|
||||
match = DISPLAY_TITLE.fullmatch(title) if isinstance(title, str) else None
|
||||
@@ -528,7 +543,7 @@ def reconcile(github: GitHub, run_id: int, max_attempts: int) -> None:
|
||||
raise ReconciliationError("convergence history omitted the requested run")
|
||||
newest = max(matching_runs, key=lambda item: timestamp(item.get("created_at"), "run"))
|
||||
if newest.get("id") != run_id:
|
||||
print(f"A newer convergence run already owns {title}; no action.")
|
||||
report_decision(f"A newer convergence run already owns {title}; no action.")
|
||||
return
|
||||
try:
|
||||
release = github.api(f"repos/{repository}/releases/tags/{tag}")
|
||||
@@ -554,21 +569,21 @@ def reconcile(github: GitHub, run_id: int, max_attempts: int) -> None:
|
||||
) < 50:
|
||||
submitted = github.post(f"repos/{repository}/actions/runs/{run_id}/rerun")
|
||||
if submitted:
|
||||
print(
|
||||
report_decision(
|
||||
f"Renewed pre-commit convergence owner {run_id} "
|
||||
f"for active source {source_run_id}."
|
||||
)
|
||||
else:
|
||||
print(
|
||||
report_decision(
|
||||
f"DRY RUN: Would renew pre-commit convergence owner {run_id} "
|
||||
f"for active source {source_run_id}."
|
||||
)
|
||||
return
|
||||
print(f"{tag} has no immutable activation commit; no convergence retry was dispatched.")
|
||||
report_decision(f"{tag} has no immutable activation commit; no convergence retry was dispatched.")
|
||||
return
|
||||
|
||||
if github.unchanged_credential_containment_block(run_id):
|
||||
print(
|
||||
report_decision(
|
||||
f"Convergence run {run_id} is held by unchanged private credential containment; "
|
||||
"no unattended retry was dispatched."
|
||||
)
|
||||
@@ -604,16 +619,16 @@ def reconcile(github: GitHub, run_id: int, max_attempts: int) -> None:
|
||||
if item.get("head_sha") == run.get("head_sha")
|
||||
)
|
||||
if attempts >= max_attempts:
|
||||
print(
|
||||
report_decision(
|
||||
f"{title} remains failed after the {attempts}-attempt retry budget; "
|
||||
"no unchanged-control retry was dispatched."
|
||||
)
|
||||
return
|
||||
submitted = github.post(f"repos/{repository}/actions/runs/{run_id}/rerun")
|
||||
if submitted:
|
||||
print(f"Re-ran current-control convergence {run_id} for committed {tag}.")
|
||||
report_decision(f"Re-ran current-control convergence {run_id} for committed {tag}.")
|
||||
else:
|
||||
print(
|
||||
report_decision(
|
||||
f"DRY RUN: Would re-run current-control convergence {run_id} "
|
||||
f"for committed {tag}."
|
||||
)
|
||||
@@ -641,12 +656,12 @@ def reconcile(github: GitHub, run_id: int, max_attempts: int) -> None:
|
||||
payload,
|
||||
)
|
||||
if submitted:
|
||||
print(
|
||||
report_decision(
|
||||
f"Dispatched fresh convergence controls after observing {default_commit} "
|
||||
f"for committed {tag}; the failed run used {run.get('head_sha')}."
|
||||
)
|
||||
else:
|
||||
print(
|
||||
report_decision(
|
||||
f"DRY RUN: Would dispatch fresh convergence controls after observing "
|
||||
f"{default_commit} for committed {tag}; the failed run used "
|
||||
f"{run.get('head_sha')}."
|
||||
@@ -708,7 +723,10 @@ def main() -> int:
|
||||
try:
|
||||
run_ids = discover(github) if args.latest else [args.run_id]
|
||||
if not run_ids:
|
||||
print("No current immutable release has unattended convergence debt.")
|
||||
report_decision(
|
||||
"No failed run selected for retry among current immutable channel heads. "
|
||||
"Mutable releases and missing convergence runs are not qualified by this check."
|
||||
)
|
||||
return 0
|
||||
for run_id in run_ids:
|
||||
assert run_id is not None
|
||||
|
||||
@@ -2,8 +2,12 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import contextlib
|
||||
import io
|
||||
import os
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
import subprocess
|
||||
@@ -256,6 +260,49 @@ class FakeGitHub:
|
||||
return False
|
||||
|
||||
|
||||
class DecisionSummaryTests(unittest.TestCase):
|
||||
def test_local_execution_needs_no_summary_file(self):
|
||||
with patch.dict(os.environ, {}, clear=True), contextlib.redirect_stdout(io.StringIO()) as output:
|
||||
subject.report_decision("No action.")
|
||||
self.assertEqual("No action.\n", output.getvalue())
|
||||
|
||||
def test_summary_appends_escaped_decisions_without_claiming_delivery(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / "summary.md"
|
||||
path.write_text("Existing summary\n", encoding="utf-8")
|
||||
with patch.dict(os.environ, {"GITHUB_STEP_SUMMARY": str(path)}):
|
||||
subject.report_decision("held <not delivered>")
|
||||
subject.report_decision("second decision")
|
||||
summary = path.read_text(encoding="utf-8")
|
||||
self.assertTrue(summary.startswith("Existing summary\n"))
|
||||
self.assertIn("held <not delivered>", summary)
|
||||
self.assertIn("second decision", summary)
|
||||
self.assertIn("not evidence of successful release convergence", summary)
|
||||
|
||||
def test_empty_discovery_does_not_claim_absent_debt(self):
|
||||
args = argparse.Namespace(repository="rcourtman/Pulse", run_id=None,
|
||||
latest=True, max_attempts=5, dry_run=True)
|
||||
with patch.object(subject, "parse_args", return_value=args), \
|
||||
patch.object(subject, "discover", return_value=[]), \
|
||||
patch.object(subject, "report_decision") as report:
|
||||
self.assertEqual(0, subject.main())
|
||||
message = report.call_args.args[0]
|
||||
self.assertIn("No failed run selected for retry", message)
|
||||
self.assertIn("missing convergence runs are not qualified", message)
|
||||
|
||||
def test_credential_hold_is_visible_without_retry(self):
|
||||
github = FakeGitHub(current_controls=True)
|
||||
github.unchanged_credential_containment_block = lambda run_id: True
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / "summary.md"
|
||||
with patch.dict(os.environ, {"GITHUB_STEP_SUMMARY": str(path)}):
|
||||
subject.reconcile(github, github.run_id, 5)
|
||||
summary = path.read_text(encoding="utf-8")
|
||||
self.assertEqual([], github.posts)
|
||||
self.assertIn("held by unchanged private credential containment", summary)
|
||||
self.assertIn("no unattended retry was dispatched", summary)
|
||||
|
||||
|
||||
class JobLogTests(unittest.TestCase):
|
||||
def test_uses_captured_sanitised_job_reader_with_private_auth(self):
|
||||
github = subject.GitHub("rcourtman/Pulse", "gh", mutate=False)
|
||||
|
||||
Reference in New Issue
Block a user