mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user