mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-26 05:29:45 +00:00
e2a3ce4bc2
A completed E2E log read exceeded the enforced eight-million-byte output bound. Keep complete fixed-container stdout/stderr and exact source/run/hash receipts in short-lived artifacts, emit only bounded escaped console tails, and retain nonzero, timed-out and unavailable collection results without suppressing them. Preserve every test tier, retry policy and existing enforcement bound. Contract-Neutral: CI-only diagnostic retention; product runtime, source-image admission and stable/probation test contracts are unchanged. Change-source: pulse-maintainer
105 lines
4.1 KiB
Python
105 lines
4.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Retain CI container diagnostics without flooding the combined Actions log.
|
|
|
|
Full stdout/stderr belongs in this run's short-lived artifact. The console gets
|
|
only a bounded, escaped tail, byte/hash identity and an honest collection result.
|
|
This observes fixed disposable E2E containers; it never starts or changes one.
|
|
"""
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
|
|
|
|
TAIL_BYTES = 4096
|
|
COMMAND_TIMEOUT_SECONDS = 30
|
|
COMMANDS = (
|
|
("containers.txt", ("docker", "ps", "-a")),
|
|
("pulse-test-server.log", ("docker", "logs", "pulse-test-server")),
|
|
("pulse-mock-github.log", ("docker", "logs", "pulse-mock-github")),
|
|
)
|
|
|
|
|
|
def checkout_identity():
|
|
identity = {
|
|
"source_sha": os.environ["GITHUB_SHA"],
|
|
"run_id": os.environ["GITHUB_RUN_ID"],
|
|
"run_attempt": os.environ["GITHUB_RUN_ATTEMPT"],
|
|
}
|
|
if not re.fullmatch(r"[0-9a-f]{40}", identity["source_sha"]):
|
|
raise ValueError("invalid E2E source identity")
|
|
if any(not re.fullmatch(r"[1-9][0-9]*", identity[key])
|
|
for key in ("run_id", "run_attempt")):
|
|
raise ValueError("invalid E2E run identity")
|
|
source = subprocess.run(
|
|
["git", "rev-parse", "HEAD"], check=True, capture_output=True,
|
|
text=True, timeout=COMMAND_TIMEOUT_SECONDS,
|
|
).stdout.strip()
|
|
if source != identity["source_sha"]:
|
|
raise ValueError("E2E checkout differs from this run")
|
|
return identity
|
|
|
|
|
|
def file_observation(path):
|
|
digest = hashlib.sha256()
|
|
with path.open("rb") as stream:
|
|
for block in iter(lambda: stream.read(65536), b""):
|
|
digest.update(block)
|
|
size = stream.tell()
|
|
stream.seek(max(0, size - TAIL_BYTES))
|
|
tail = stream.read(TAIL_BYTES)
|
|
return size, digest.hexdigest(), tail
|
|
|
|
|
|
def collect(directory, identity, *, registration=False, run=subprocess.run, console=None):
|
|
console = sys.stdout if console is None else console
|
|
# Never overwrite another attempt's evidence, including through a symlink.
|
|
directory.mkdir(parents=True, exist_ok=False)
|
|
records = []
|
|
for filename, command in COMMANDS[:2] if registration else COMMANDS:
|
|
path = directory / filename
|
|
record = {"file": filename, "exit_code": None, "collection_complete": False}
|
|
with path.open("xb") as stream:
|
|
try:
|
|
result = run(command, stdout=stream, stderr=subprocess.STDOUT,
|
|
timeout=COMMAND_TIMEOUT_SECONDS, check=False)
|
|
record.update(exit_code=result.returncode,
|
|
collection_complete=result.returncode == 0)
|
|
except subprocess.TimeoutExpired:
|
|
# subprocess.run stops and waits for its Docker CLI on timeout;
|
|
# TimeoutExpired exposes no terminal exit. Do not invent one.
|
|
record["failure"] = "timeout"
|
|
except OSError:
|
|
record["failure"] = "spawn"
|
|
size, digest, tail = file_observation(path)
|
|
record.update(bytes=size, sha256=digest)
|
|
records.append(record)
|
|
# JSON escaping prevents log text becoming Actions commands. Never
|
|
# mistake this bounded excerpt for the complete artifact bytes.
|
|
print(json.dumps({**record, "console_tail_bytes": len(tail),
|
|
"console_tail_truncated": size > len(tail),
|
|
"console_tail": tail.decode("utf-8", errors="replace")}),
|
|
file=console, flush=True)
|
|
complete = all(record["collection_complete"] for record in records)
|
|
manifest = {"schema_version": 1, **identity, "collection_complete": complete,
|
|
"records": records}
|
|
(directory / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n")
|
|
return 0 if complete else 1
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("output", type=Path)
|
|
parser.add_argument("--registration", action="store_true")
|
|
arguments = parser.parse_args()
|
|
return collect(arguments.output, checkout_identity(), registration=arguments.registration)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|