From e5f00a8b39fec7c417782132be0f745f40953f7e Mon Sep 17 00:00:00 2001 From: "torlando-agent[bot]" <281092095+torlando-agent[bot]@users.noreply.github.com> Date: Sun, 2 Aug 2026 03:57:34 +0000 Subject: [PATCH] ci: audit release flags and dependency pins --- .github/workflows/build-check.yml | 4 + .github/workflows/release-firmware.yml | 3 + .../test_release_build_contract.py | 2 + tools/audit_release_build.py | 115 ++++++++++++++++++ 4 files changed, 124 insertions(+) create mode 100644 tools/audit_release_build.py diff --git a/.github/workflows/build-check.yml b/.github/workflows/build-check.yml index 7d4fe055..9d873419 100644 --- a/.github/workflows/build-check.yml +++ b/.github/workflows/build-check.yml @@ -28,6 +28,10 @@ jobs: - name: Build ${{ matrix.environment }} run: pio run -e ${{ matrix.environment }} + - name: Audit release artifact + if: matrix.environment == 'tdeck-release' + run: python tools/audit_release_build.py + - name: Upload firmware artifact if: matrix.environment == 'tdeck-release' uses: actions/upload-artifact@v4 diff --git a/.github/workflows/release-firmware.yml b/.github/workflows/release-firmware.yml index 0b920a6b..2ab52044 100644 --- a/.github/workflows/release-firmware.yml +++ b/.github/workflows/release-firmware.yml @@ -33,6 +33,9 @@ jobs: - name: Build release-qualified T-Deck firmware run: pio run -e tdeck-release + - name: Audit release artifact + run: python tools/audit_release_build.py + - name: Determine version string id: version run: | diff --git a/tests/build_scripts/test_release_build_contract.py b/tests/build_scripts/test_release_build_contract.py index 8b5a235e..dc2b61b7 100644 --- a/tests/build_scripts/test_release_build_contract.py +++ b/tests/build_scripts/test_release_build_contract.py @@ -47,8 +47,10 @@ def test_ci_and_deployment_build_the_release_environment(): assert "environment: [tdeck, tdeck-release]" in build_check assert "matrix.environment == 'tdeck-release'" in build_check assert ".pio/build/tdeck-release/firmware.bin" in build_check + assert "python tools/audit_release_build.py" in build_check assert "pio run -e tdeck-release" in release + assert "python tools/audit_release_build.py" in release assert ".pio/build/tdeck-release/bootloader.bin" in release assert ".pio/build/tdeck-release/partitions.bin" in release assert ".pio/build/tdeck-release/firmware.bin" in release diff --git a/tools/audit_release_build.py b/tools/audit_release_build.py new file mode 100644 index 00000000..d62e9ee4 --- /dev/null +++ b/tools/audit_release_build.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +"""Fail closed when a tdeck-release build is not release-qualified.""" + +from __future__ import annotations + +import json +from pathlib import Path +import shutil +import subprocess + + +ROOT = Path(__file__).resolve().parents[1] +ENVIRONMENT = "tdeck-release" +BUILD = ROOT / ".pio" / "build" / ENVIRONMENT +EXCLUDED_DEFINITIONS = ( + "PYXIS_TEST_HOOKS", + "PYXIS_TEST_TCP_HOST", + "PYXIS_TEST_TCP_PORT", + "MEMORY_INSTRUMENTATION_ENABLED", + "BOOT_PROFILING_ENABLED", +) +EXCLUDED_STRINGS = ( + b"T:CALL_PROFILE", + b"T:CALL_INJECT", + b"T:DEST", + b"PYXIS_TEST_HOOKS", + b"Boot Profile Summary", + b"Memory monitor started", +) +EXCLUDED_SYMBOLS = ( + "test_call_", + "process_test_serial_command", + "MemoryMonitor::", + "BootProfiler::", +) +PINNED_DEPENDENCIES = { + "microReticulum": "6054f6ba82367628a85cd07fcb668b95e947f046", + "microLXMF": "d9bbc04cf69bfa9b555c3f293b89b440b4820518", + "microStore": "c5fb69d68229e684c7fbd17692a67ae8193b84e2", +} + + +def run(*args: str, cwd: Path = ROOT) -> str: + return subprocess.run( + args, + cwd=cwd, + check=True, + text=True, + capture_output=True, + ).stdout.strip() + + +def require(condition: bool, message: str) -> None: + if not condition: + raise SystemExit(f"release audit failed: {message}") + + +def package_commit(name: str) -> str: + package = ROOT / ".pio" / "libdeps" / ENVIRONMENT / name + require(package.is_dir(), f"missing dependency package {name}") + top = Path(run("git", "rev-parse", "--show-toplevel", cwd=package)).resolve() + require(top == package.resolve(), f"{name} is not an independently verifiable VCS package") + return run("git", "rev-parse", "HEAD", cwd=package) + + +def locate_nm() -> str: + direct = shutil.which("xtensa-esp32s3-elf-nm") + if direct: + return direct + candidate = ( + Path.home() + / ".platformio/packages/toolchain-xtensa-esp32s3/bin/xtensa-esp32s3-elf-nm" + ) + require(candidate.is_file(), "xtensa-esp32s3-elf-nm not found") + return str(candidate) + + +def main() -> None: + run("pio", "run", "-e", ENVIRONMENT, "-t", "compiledb") + + compile_commands = ROOT / "compile_commands.json" + commands = json.loads(compile_commands.read_text()) + main_compile = next( + entry for entry in commands if Path(entry["file"]).as_posix().endswith("src/main.cpp") + ) + command = main_compile.get("command") or " ".join(main_compile["arguments"]) + present = [flag for flag in EXCLUDED_DEFINITIONS if flag in command] + require(not present, f"excluded compiler definitions present: {', '.join(present)}") + + firmware = BUILD / "firmware.bin" + elf = BUILD / "firmware.elf" + require(firmware.is_file(), "firmware.bin missing") + require(elf.is_file(), "firmware.elf missing") + + firmware_bytes = firmware.read_bytes() + strings_present = [value.decode() for value in EXCLUDED_STRINGS if value in firmware_bytes] + require(not strings_present, f"excluded firmware strings present: {', '.join(strings_present)}") + + symbols = run(locate_nm(), "-C", str(elf)) + symbols_present = [value for value in EXCLUDED_SYMBOLS if value in symbols] + require(not symbols_present, f"excluded ELF symbols present: {', '.join(symbols_present)}") + + for name, expected in PINNED_DEPENDENCIES.items(): + actual = package_commit(name) + require(actual == expected, f"{name} resolved to {actual}, expected {expected}") + + compile_commands.unlink() + print("release audit passed") + print(f"firmware_size={len(firmware_bytes)}") + for name, expected in PINNED_DEPENDENCIES.items(): + print(f"{name}={expected}") + + +if __name__ == "__main__": + main()