Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 88 additions & 9 deletions build.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import shutil
import subprocess
import sys
import tempfile
import time
from dataclasses import dataclass
from pathlib import Path
Expand Down Expand Up @@ -169,6 +170,7 @@ class Module:
"linux-x64": ENCRYPTLY_DIR / "linux-x64" / "encryptly",
"linux-arm64": ENCRYPTLY_DIR / "linux-arm64" / "encryptly",
"macos-arm64": ENCRYPTLY_DIR / "macos-arm64" / "encryptly",
"macos-x64": ENCRYPTLY_DIR / "macos-x64" / "encryptly",
"windows-x64": ENCRYPTLY_DIR / "windows-x64" / "encryptly.exe",
"windows-arm64": ENCRYPTLY_DIR / "windows-arm64" / "encryptly.exe",
}
Expand Down Expand Up @@ -277,6 +279,83 @@ def color(text: str, code: str) -> str:
return text
return f"{code}{text}{Colors.RESET}"


def repo_relative(path: Path) -> str:
"""Return a repository-relative path with POSIX separators for metadata."""
return path.relative_to(ROOT).as_posix()


def _redaction_values() -> list[tuple[str, str]]:
values: list[tuple[str, str]] = []
candidates = [
(str(ROOT), "<repo>"),
(str(Path.home()), "<home>"),
(tempfile.gettempdir(), "<temp>"),
(getpass.getuser(), "<user>"),
(platform.node(), "<host>"),
]
for raw, replacement in candidates:
if raw:
values.append((raw, replacement))
if os.sep != "/":
values.append((raw.replace(os.sep, "/"), replacement))
return sorted(values, key=lambda item: len(item[0]), reverse=True)


def redact_metadata_text(value: str) -> str:
redacted = value
for needle, replacement in _redaction_values():
redacted = redacted.replace(needle, replacement)
return redacted


def metadata_path_value(value: Optional[str]) -> Optional[str]:
"""Normalize artifact paths for JSON metadata without leaking local paths."""
if value is None:
return None
try:
path = Path(value).resolve()
return repo_relative(path)
except Exception:
return redact_metadata_text(value)


def validate_diagnostic_metadata(metadata_path: Path, root: Path = ROOT) -> list[str]:
"""Return clear validation errors for diagnostic JSON/logd pairing."""
errors: list[str] = []
if not metadata_path.exists():
return [f"diagnostic metadata is missing: {metadata_path}"]

try:
report = json.loads(metadata_path.read_text(encoding="utf-8"))
except Exception as exc:
return [f"diagnostic metadata is not valid JSON: {exc}"]

diagnostic_logd = report.get("diagnostic_logd")
if not diagnostic_logd:
if report.get("diagnostic_logd_error"):
return errors
return ["diagnostic metadata is missing diagnostic_logd"]

logd_entries = diagnostic_logd if isinstance(diagnostic_logd, list) else [diagnostic_logd]
for entry in logd_entries:
if not isinstance(entry, str):
errors.append(f"diagnostic_logd entry is not a string: {entry!r}")
continue
if "\\" in entry:
errors.append(f"diagnostic_logd must use '/' separators: {entry}")
if Path(entry).is_absolute():
errors.append(f"diagnostic_logd must be repository-relative: {entry}")
continue
logd_path = root / entry
if not logd_path.exists():
errors.append(f"diagnostic .logd artifact is missing: {entry}")
if logd_path.suffix != ".logd":
errors.append(f"diagnostic artifact is not a .logd file: {entry}")

return errors


def check_prerequisites() -> list[str]:
required = {
"cargo": "Rust",
Expand Down Expand Up @@ -500,7 +579,7 @@ def build_diagnostic_report(

decrypt_target = logd_relpaths[0] if logd_relpaths and len(logd_relpaths) == 1 else None
if logd_relpaths and len(logd_relpaths) > 1:
decrypt_target = str((DIAGNOSTIC_DIR / f"build-{commit_id}.logd").relative_to(ROOT))
decrypt_target = repo_relative(DIAGNOSTIC_DIR / f"build-{commit_id}.logd")

report = {
"generated_at": datetime.datetime.now(datetime.timezone.utc).isoformat(),
Expand All @@ -523,8 +602,8 @@ def build_diagnostic_report(
"name": name,
"status": "PASS" if success else "FAIL",
"elapsed_seconds": round(elapsed, 3),
"artifact": binary,
"output": output,
"artifact": metadata_path_value(binary),
"output": redact_metadata_text(output),
}
for name, success, elapsed, output, binary in results
],
Expand All @@ -539,7 +618,7 @@ def build_diagnostic_report(

def write_diagnostic_report(metadata_path: Path, report: dict) -> None:
metadata_path.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
print(f" {color('✓', Colors.GREEN)} {metadata_path.relative_to(ROOT)} created")
print(f" {color('✓', Colors.GREEN)} {repo_relative(metadata_path)} created")


def commit_diagnostic_artifacts(paths: list[Path], commit_id: str) -> bool:
Expand All @@ -549,7 +628,7 @@ def commit_diagnostic_artifacts(paths: list[Path], commit_id: str) -> bool:
print(f" {color('✗', Colors.RED)} No diagnostic artifacts found to commit")
return False

relpaths = [str(path.relative_to(ROOT)) for path in existing]
relpaths = [repo_relative(path) for path in existing]
status = subprocess.run(
["git", "status", "--porcelain", "--", *relpaths],
cwd=str(ROOT),
Expand Down Expand Up @@ -703,8 +782,8 @@ def generate_logd(

safe_pw = sr.stdout.strip()
logd_files = split_diagnostic_logd(logd_path)
logd_relpaths = [str(path.relative_to(ROOT)) for path in logd_files]
decrypt_target = logd_relpaths[0] if len(logd_relpaths) == 1 else str(logd_path.relative_to(ROOT))
logd_relpaths = [repo_relative(path) for path in logd_files]
decrypt_target = logd_relpaths[0] if len(logd_relpaths) == 1 else repo_relative(logd_path)
write_diagnostic_report(
metadata_path,
build_diagnostic_report(
Expand All @@ -719,7 +798,7 @@ def generate_logd(
for path in logd_files:
size_kb = path.stat().st_size / 1024.0
print(
f" {color('✓', Colors.GREEN)} {path.relative_to(ROOT)} created "
f" {color('✓', Colors.GREEN)} {repo_relative(path)} created "
f"({size_kb:.1f} KiB)"
)
if len(logd_files) > 1:
Expand All @@ -737,7 +816,7 @@ def generate_logd(
print(f" diagnostic log file(s) and metadata file with this password.")
if len(logd_files) > 1:
print(f" Reassemble chunks in order before unpacking:")
print(f" cat {' '.join(logd_relpaths)} > {logd_path.relative_to(ROOT)}")
print(f" cat {' '.join(logd_relpaths)} > {repo_relative(logd_path)}")
print(f" {color(safe_pw, Colors.CYAN)}")
print(f" {color(f'encryptly unpack {decrypt_target} <outdir> --password {safe_pw}', Colors.GRAY)}")
return True
Expand Down
87 changes: 87 additions & 0 deletions diagnostic/build-fc78a64d.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
{
"generated_at": "2026-07-08T11:45:48.455656+00:00",
"commit": "fc78a64d",
"diagnostic_logd": "diagnostic/build-fc78a64d.logd",
"diagnostic_logd_error": null,
"message_blocker": null,
"chunked": false,
"chunk_size_bytes": null,
"password": "4f9406258c55071a2684",
"decrypt_command": "encryptly unpack diagnostic/build-fc78a64d.logd <outdir> --password 4f9406258c55071a2684",
"total_modules": 10,
"passed": 2,
"failed": 8,
"modules": [
{
"name": "backend",
"status": "FAIL",
"elapsed_seconds": 0,
"artifact": null,
"output": "Command not found: [Errno 2] No such file or directory: 'cargo'"
},
{
"name": "frontend",
"status": "PASS",
"elapsed_seconds": 14.831,
"artifact": "frontend/dist",
"output": "> tent-frontend@0.0.0 build\n> tsc -b && vite build\n\nvite v6.4.3 building for production...\ntransforming...\n\u2713 100 modules transformed.\nrendering chunks...\ncomputing gzip size...\ndist/index.html 0.62 kB \u2502 gzip: 0.34 kB\ndist/assets/state-BkjSKDbY.js 8.91 kB \u2502 gzip: 3.55 kB \u2502 map: 57.15 kB\ndist/assets/vendor-CREcWLHI.js 48.93 kB \u2502 gzip: 17.22 kB \u2502 map: 481.27 kB\ndist/assets/index-CyxcoTyU.js 231.32 kB \u2502 gzip: 72.02 kB \u2502 map: 1,044.42 kB\n\u2713 built in 2.04s"
},
{
"name": "market",
"status": "FAIL",
"elapsed_seconds": 0,
"artifact": null,
"output": "Command not found: [Errno 2] No such file or directory: 'go'"
},
{
"name": "frailbox",
"status": "FAIL",
"elapsed_seconds": 1.258,
"artifact": null,
"output": "gcc -Wall -Wextra -Wpedantic -std=c2x -O2 -g -D_FORTIFY_SOURCE=3 -fstack-protector-strong -fPIE -Iinclude -MMD -MP -c src/arena.c -o build/src/arena.o\nsrc/arena.c:17:23: error: use of undeclared identifier 'MAP_HUGETLB'\n 17 | mmap_flags |= MAP_HUGETLB;\n | ^~~~~~~~~~~\nsrc/arena.c:179:17: warning: comparison of distinct pointer types ('const void *' and 'char *') [-Wcompare-distinct-pointer-types]\n 179 | ptr < (char *)region->start + region->size) {\n | ~~~ ^ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n1 warning and 1 error generated.\nmake: *** [build/src/arena.o] Error 1"
},
{
"name": "engine",
"status": "FAIL",
"elapsed_seconds": 0,
"artifact": null,
"output": "Command not found: [Errno 2] No such file or directory: 'cmake'"
},
{
"name": "compliance",
"status": "FAIL",
"elapsed_seconds": 0.039,
"artifact": null,
"output": "The operation couldn\u2019t be completed. Unable to locate a Java Runtime.\nPlease visit http://www.java.com for information on installing Java."
},
{
"name": "v2-market-stream",
"status": "PASS",
"elapsed_seconds": 0.329,
"artifact": null,
"output": "Syntax OK"
},
{
"name": "nfc-scanner",
"status": "FAIL",
"elapsed_seconds": 0,
"artifact": null,
"output": "Command not found: [Errno 2] No such file or directory: 'luac'"
},
{
"name": "openapi-haskell",
"status": "FAIL",
"elapsed_seconds": 0,
"artifact": null,
"output": "Command not found: [Errno 2] No such file or directory: 'ghc'"
},
{
"name": "openapi-tools",
"status": "FAIL",
"elapsed_seconds": 0,
"artifact": null,
"output": "Command not found: [Errno 2] No such file or directory: 'luac'"
}
],
"pr_note": "Include the encrypted diagnostic logd artifact(s): diagnostic/build-fc78a64d.logd. The encrypted .logd is the required diagnostic content for PR review; this JSON file is metadata. Maintainers may ask you to remove these diagnostic artifacts before merging."
}
Binary file added diagnostic/build-fc78a64d.logd
Binary file not shown.
123 changes: 123 additions & 0 deletions tests/test_diagnostics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import getpass
import json
import platform
import tempfile
import unittest
from pathlib import Path

import build


class DiagnosticMetadataTests(unittest.TestCase):
def test_report_redacts_local_identity_and_normalizes_artifact_paths(self):
repo_artifact = build.ROOT / "backend" / "target" / "debug" / "backend"
output = "\n".join(
[
f"repo={build.ROOT}",
f"home={Path.home()}",
f"temp={tempfile.gettempdir()}",
f"user={getpass.getuser()}",
f"host={platform.node()}",
]
)

report = build.build_diagnostic_report(
[("backend", True, 1.234, output, str(repo_artifact))],
"deadbeef",
logd_relpaths=["diagnostic/build-deadbeef.logd"],
password="pw",
)

module = report["modules"][0]
self.assertEqual(module["artifact"], "backend/target/debug/backend")
self.assertNotIn("\\", module["artifact"])

encoded = json.dumps(report)
self.assertNotIn(str(build.ROOT), encoded)
self.assertNotIn(str(Path.home()), encoded)
self.assertNotIn(tempfile.gettempdir(), encoded)
self.assertNotIn(getpass.getuser(), encoded)
if platform.node():
self.assertNotIn(platform.node(), encoded)

def test_validate_diagnostic_metadata_accepts_matching_logd_pair(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
diagnostic_dir = root / "diagnostic"
diagnostic_dir.mkdir()
(diagnostic_dir / "build-deadbeef.logd").write_bytes(b"encrypted")
metadata_path = diagnostic_dir / "build-deadbeef.json"
metadata_path.write_text(
json.dumps({"diagnostic_logd": "diagnostic/build-deadbeef.logd"}),
encoding="utf-8",
)

self.assertEqual(build.validate_diagnostic_metadata(metadata_path, root), [])

def test_validate_diagnostic_metadata_reports_missing_json(self):
missing = Path(tempfile.gettempdir()) / "does-not-exist-diagnostic.json"

errors = build.validate_diagnostic_metadata(missing, missing.parent)

self.assertEqual(len(errors), 1)
self.assertIn("metadata is missing", errors[0])

def test_validate_diagnostic_metadata_reports_missing_logd(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
diagnostic_dir = root / "diagnostic"
diagnostic_dir.mkdir()
metadata_path = diagnostic_dir / "build-deadbeef.json"
metadata_path.write_text(
json.dumps({"diagnostic_logd": "diagnostic/build-deadbeef.logd"}),
encoding="utf-8",
)

errors = build.validate_diagnostic_metadata(metadata_path, root)

self.assertEqual(len(errors), 1)
self.assertIn(".logd artifact is missing", errors[0])

def test_validate_diagnostic_metadata_rejects_absolute_or_backslash_paths(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
diagnostic_dir = root / "diagnostic"
diagnostic_dir.mkdir()
metadata_path = diagnostic_dir / "build-deadbeef.json"
metadata_path.write_text(
json.dumps(
{
"diagnostic_logd": [
str(root / "diagnostic" / "build-deadbeef.logd"),
"diagnostic\\build-deadbeef.logd",
]
}
),
encoding="utf-8",
)

errors = build.validate_diagnostic_metadata(metadata_path, root)

self.assertTrue(any("repository-relative" in error for error in errors))
self.assertTrue(any("'/' separators" in error for error in errors))

def test_validate_diagnostic_metadata_rejects_mismatched_artifact_extension(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
diagnostic_dir = root / "diagnostic"
diagnostic_dir.mkdir()
(diagnostic_dir / "build-deadbeef.txt").write_text("not logd", encoding="utf-8")
metadata_path = diagnostic_dir / "build-deadbeef.json"
metadata_path.write_text(
json.dumps({"diagnostic_logd": "diagnostic/build-deadbeef.txt"}),
encoding="utf-8",
)

errors = build.validate_diagnostic_metadata(metadata_path, root)

self.assertEqual(len(errors), 1)
self.assertIn("not a .logd", errors[0])


if __name__ == "__main__":
unittest.main()