fix(release): inspect convergence logs through safe Actions reader

Scheduled reconciliation fails when gh api refuses ANSI-bearing Actions logs. Use the dedicated sanitising log reader without disabling terminal protection, preserving private authentication and fail-closed evidence handling. Focused reconciliation and policy tests pass; a read-only live job probe retains failure evidence without ESC bytes.

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-09-05 04:32:38 +01:00
parent 193ead50fd
commit ec462ad964
3 changed files with 55 additions and 6 deletions
@@ -5097,3 +5097,19 @@ allowlist, the drift refusal, the hotfix path, and the minor soak;
summaries whose scope, format, and attachments the server fixes. The delivery
surface (`/api/admin/reports/schedules`) and tenant-local storage are
unchanged.
### Convergence log-reader compatibility (5 September 2026)
Scheduled reconciliation run 33939926237 failed when `gh api` refused terminal
escape sequences in an Actions job log. Credential-containment evidence now
uses `gh run view --repo … --job … --log`, whose dedicated log reader neutralises
terminal controls. It does not enable `--allow-escape-sequences`. Private logs
remain captured for marker inspection, not printed; authentication and log-read
failures retain the existing fail-closed behaviour. This changes evidence
transport only, not containment, retry budgets or promotion authority.
Verification: 27 reconciliation tests and 46 release-promotion-policy tests
passed. A read-only probe of job 101235205647 retained the expected failure text
without ESC bytes. Private containment classification and successful scheduled
reconciliation still require post-integration evidence; this is not customer
convergence or release qualification.
@@ -325,14 +325,19 @@ class GitHub:
return value
def job_log(self, repository: str, job_id: int, *, token: str = "") -> str:
# Actions logs routinely contain ANSI colour sequences. gh api refuses
# those bytes for terminal safety; use the dedicated log reader, which
# neutralises control sequences, rather than disabling that protection.
# Keep captured private logs internal: callers inspect only a marker.
return self._run(
[
"api",
"-H",
"Accept: application/vnd.github+json",
"-H",
"X-GitHub-Api-Version: 2026-03-10",
f"repos/{repository}/actions/jobs/{job_id}/logs",
"run",
"view",
"--repo",
repository,
"--job",
str(job_id),
"--log",
],
token=token,
)
@@ -5,6 +5,8 @@ from __future__ import annotations
import contextlib
import io
import unittest
from unittest.mock import patch
import subprocess
import reconcile_release_convergence as subject
@@ -253,6 +255,32 @@ class FakeGitHub:
return False
class JobLogTests(unittest.TestCase):
def test_uses_captured_sanitised_job_reader_with_private_auth(self):
github = subject.GitHub("rcourtman/Pulse", "gh", mutate=False)
log = "Require credential containment\tcheck\tcredential containment gate: BLOCKED\n"
with patch.object(subject.subprocess, "run", return_value=subprocess.CompletedProcess(
[], 0, stdout=log, stderr=""
)) as command, contextlib.redirect_stdout(io.StringIO()) as output:
self.assertEqual(log, github.job_log(
subject.PRIVATE_REPOSITORY, 123, token="test-private-token"
))
self.assertEqual("", output.getvalue())
args, kwargs = command.call_args
self.assertEqual(["gh", "run", "view", "--repo", subject.PRIVATE_REPOSITORY,
"--job", "123", "--log"], args[0])
self.assertTrue(kwargs["capture_output"])
self.assertEqual("test-private-token", kwargs["env"]["GH_TOKEN"])
def test_unavailable_job_logs_fail_closed(self):
github = subject.GitHub("rcourtman/Pulse", "gh", mutate=False)
with patch.object(subject.subprocess, "run", return_value=subprocess.CompletedProcess(
[], 1, stdout="", stderr="log unavailable"
)):
with self.assertRaisesRegex(subject.ReconciliationError, "log unavailable"):
github.job_log(subject.PRIVATE_REPOSITORY, 123)
class CredentialContainmentTests(unittest.TestCase):
def github(
self,