chore: bump device-protocol to master (bf8646b) + update nanopb options #229
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # KeepKey Firmware CI Pipeline | |
| # | |
| # FAIL FAST — quick checks gate everything, no wasted compute. | |
| # | |
| # Stage 1: GATE (seconds, no Docker) | |
| # ├─ lint-format clang-format diff check | |
| # ├─ static-analysis cppcheck static analysis | |
| # ├─ secret-scan gitleaks credential detection | |
| # └─ check-submodules verify all deps present | |
| # | |
| # Stage 2: BUILD (parallel, gated by Stage 1) | |
| # ├─ build-emulator Docker image → artifact | |
| # └─ build-arm-firmware cross-compile → .bin/.elf (downloadable) | |
| # | |
| # Stage 3: TEST (parallel, gated by Stage 2) | |
| # ├─ unit-tests GoogleTest (make xunit) | |
| # └─ python-integration full test suite | |
| # | |
| # Stage 4: PUBLISH (manual trigger, all tests must pass) | |
| # └─ publish-emulator DockerHub push (workflow_dispatch only) | |
| name: CI | |
| on: | |
| push: | |
| branches: [master, develop, 'feature/**', 'fix/**', 'release/**', 'hotfix/**'] | |
| pull_request: | |
| branches: [master, develop] | |
| workflow_dispatch: | |
| inputs: | |
| publish_emulator: | |
| description: 'Publish emulator image to DockerHub' | |
| required: false | |
| type: boolean | |
| default: false | |
| env: | |
| BASE_IMAGE: kktech/firmware:v15 | |
| EMU_IMAGE: kkemu-ci | |
| jobs: | |
| # ═══════════════════════════════════════════════════════════ | |
| # STAGE 1: GATE — kill bad PRs in seconds | |
| # ═══════════════════════════════════════════════════════════ | |
| lint-format: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 3 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha || github.sha }} | |
| - name: Install clang-format-20 (pinned stable) | |
| run: | | |
| wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - | |
| echo "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-20 main" | sudo tee /etc/apt/sources.list.d/llvm.list | |
| sudo apt-get update -qq && sudo apt-get install -y -qq clang-format-20 | |
| sudo ln -sf /usr/bin/clang-format-20 /usr/bin/clang-format | |
| - name: Check code formatting | |
| run: | | |
| echo "Using: $(clang-format --version)" | |
| FAILED=0 | |
| for f in $(find include/keepkey lib/firmware lib/board lib/transport/src \ | |
| -name '*.c' -o -name '*.h' 2>/dev/null | grep -v generated | grep -v '.pb.'); do | |
| if ! clang-format --style=file --dry-run --Werror "$f" 2>/dev/null; then | |
| echo "::warning file=$f::Formatting differs from .clang-format" | |
| FAILED=1 | |
| fi | |
| done | |
| if [ "$FAILED" = "1" ]; then | |
| echo "" | |
| echo "::error::Code formatting check failed. Run: clang-format -i <file> to fix." | |
| exit 1 | |
| fi | |
| secret-scan: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 2 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha || github.sha }} | |
| fetch-depth: 0 | |
| - name: Install gitleaks | |
| run: | | |
| GITLEAKS_VERSION=$(curl -sSf https://api.github.com/repos/gitleaks/gitleaks/releases/latest \ | |
| | grep -oP '"tag_name":\s*"v\K[^"]+') | |
| curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \ | |
| | tar -xz -C /usr/local/bin gitleaks | |
| gitleaks version | |
| - name: Run gitleaks | |
| run: gitleaks detect --source . --verbose --redact | |
| static-analysis: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha || github.sha }} | |
| submodules: false | |
| - name: Install cppcheck | |
| run: sudo apt-get update && sudo apt-get install -y cppcheck | |
| - name: Run cppcheck | |
| run: | | |
| cppcheck \ | |
| --enable=warning,style,performance,portability \ | |
| --std=c11 \ | |
| --platform=unspecified \ | |
| --inconclusive \ | |
| --force \ | |
| --inline-suppr \ | |
| --suppressions-list=.cppcheck-suppressions \ | |
| -I include \ | |
| -I deps/crypto/trezor-firmware/crypto \ | |
| -I deps/device-protocol \ | |
| -DSTM32F2=1 \ | |
| -DUSE_ETHEREUM=1 \ | |
| -DUSE_KECCAK=1 \ | |
| -DUSE_NANO=1 \ | |
| -DPB_FIELD_16BIT=1 \ | |
| -DEMULATOR=1 \ | |
| --template='::warning file={file},line={line},col={column}::{severity}: {message} [{id}]' \ | |
| --output-file=cppcheck_report.txt \ | |
| --error-exitcode=1 \ | |
| lib/ include/keepkey/ tools/ 2>&1; CPPCHECK_RC=$? | |
| # Count issues by severity | |
| ERRORS=$(grep -c '\berror:' cppcheck_report.txt 2>/dev/null || true) | |
| WARNINGS=$(grep -c '\bwarning:' cppcheck_report.txt 2>/dev/null || true) | |
| STYLE=$(grep -c '\bstyle:' cppcheck_report.txt 2>/dev/null || true) | |
| PERF=$(grep -c '\bperformance:' cppcheck_report.txt 2>/dev/null || true) | |
| PORT=$(grep -c '\bportability:' cppcheck_report.txt 2>/dev/null || true) | |
| : "${ERRORS:=0}" "${WARNINGS:=0}" "${STYLE:=0}" "${PERF:=0}" "${PORT:=0}" | |
| TOTAL=$((ERRORS + WARNINGS + STYLE + PERF + PORT)) | |
| echo "## cppcheck summary" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Severity | Count |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "|----------|-------|" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| error | $ERRORS |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| warning | $WARNINGS |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| style | $STYLE |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| performance | $PERF |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| portability | $PORT |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| **total** | **$TOTAL** |" >> "$GITHUB_STEP_SUMMARY" | |
| # Print findings as GitHub annotations | |
| cat cppcheck_report.txt | |
| if [ "$CPPCHECK_RC" -ne 0 ]; then | |
| echo "::error::cppcheck exited with error code $CPPCHECK_RC" | |
| exit 1 | |
| fi | |
| if [ "$TOTAL" -gt 0 ]; then | |
| echo "::error::cppcheck found $TOTAL issue(s) — zero-warning policy enforced" | |
| exit 1 | |
| fi | |
| echo "cppcheck: clean — zero findings" | |
| - name: Upload cppcheck report | |
| uses: actions/upload-artifact@v7 | |
| if: always() | |
| with: | |
| name: cppcheck-report | |
| path: cppcheck_report.txt | |
| retention-days: 30 | |
| check-submodules: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 2 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha || github.sha }} | |
| - name: Verify submodules are declared | |
| run: | | |
| echo "Checking required submodule declarations..." | |
| REQUIRED=( | |
| "deps/crypto/trezor-firmware" | |
| "deps/device-protocol" | |
| "deps/python-keepkey" | |
| "deps/googletest" | |
| "deps/qrenc/QR-Code-generator" | |
| "deps/sca-hardening/SecAESSTM32" | |
| ) | |
| FAILED=0 | |
| for sub in "${REQUIRED[@]}"; do | |
| if ! grep -q "$sub" .gitmodules; then | |
| echo "::error::Missing required submodule: $sub" | |
| FAILED=1 | |
| else | |
| echo " ✓ $sub" | |
| fi | |
| done | |
| [ "$FAILED" = "0" ] || exit 1 | |
| # ═══════════════════════════════════════════════════════════ | |
| # STAGE 2: BUILD — compile only after gate passes | |
| # ═══════════════════════════════════════════════════════════ | |
| build-emulator: | |
| needs: [lint-format, static-analysis, check-submodules, secret-scan] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha || github.sha }} | |
| - name: Init submodules | |
| run: | | |
| git submodule update --init deps/crypto/trezor-firmware | |
| git submodule update --init deps/device-protocol | |
| git submodule update --init --recursive deps/python-keepkey | |
| git submodule update --init deps/googletest | |
| git submodule update --init deps/qrenc/QR-Code-generator | |
| git submodule update --init deps/sca-hardening/SecAESSTM32 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Cache base image | |
| id: cache-base | |
| uses: actions/cache@v5 | |
| with: | |
| path: /tmp/base-image.tar | |
| key: base-image-${{ env.BASE_IMAGE }} | |
| - name: Pull and cache base image | |
| if: steps.cache-base.outputs.cache-hit != 'true' | |
| run: | | |
| docker pull ${{ env.BASE_IMAGE }} | |
| docker save ${{ env.BASE_IMAGE }} -o /tmp/base-image.tar | |
| - name: Load base image from cache | |
| if: steps.cache-base.outputs.cache-hit == 'true' | |
| run: docker load -i /tmp/base-image.tar | |
| - name: Build emulator image | |
| run: | | |
| docker build \ | |
| -t ${{ env.EMU_IMAGE }} \ | |
| -f scripts/emulator/Dockerfile \ | |
| . | |
| - name: Save emulator image | |
| run: docker save ${{ env.EMU_IMAGE }} -o /tmp/emu-image.tar | |
| - name: Upload emulator image artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: emu-image | |
| path: /tmp/emu-image.tar | |
| retention-days: 1 | |
| build-arm-firmware: | |
| needs: [lint-format, static-analysis, check-submodules, secret-scan] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha || github.sha }} | |
| - name: Init submodules | |
| run: | | |
| git submodule update --init deps/crypto/trezor-firmware | |
| git submodule update --init deps/device-protocol | |
| git submodule update --init --recursive deps/python-keepkey | |
| git submodule update --init deps/googletest | |
| git submodule update --init deps/qrenc/QR-Code-generator | |
| git submodule update --init deps/sca-hardening/SecAESSTM32 | |
| - name: Cache base image | |
| id: cache-base | |
| uses: actions/cache@v5 | |
| with: | |
| path: /tmp/base-image.tar | |
| key: base-image-${{ env.BASE_IMAGE }} | |
| - name: Pull and cache base image | |
| if: steps.cache-base.outputs.cache-hit != 'true' | |
| run: | | |
| docker pull ${{ env.BASE_IMAGE }} | |
| docker save ${{ env.BASE_IMAGE }} -o /tmp/base-image.tar | |
| - name: Load base image from cache | |
| if: steps.cache-base.outputs.cache-hit == 'true' | |
| run: docker load -i /tmp/base-image.tar | |
| - name: Extract firmware version | |
| id: version | |
| run: | | |
| FW_VERSION=$(sed -n '/^project/,/)/p' CMakeLists.txt | grep -oP '\d+\.\d+\.\d+') | |
| GIT_SHORT=$(git rev-parse --short HEAD) | |
| echo "fw_version=${FW_VERSION}" >> "$GITHUB_OUTPUT" | |
| echo "git_short=${GIT_SHORT}" >> "$GITHUB_OUTPUT" | |
| echo "Firmware version: ${FW_VERSION} (${GIT_SHORT})" | |
| - name: Cross-compile firmware for ARM | |
| run: | | |
| docker run --rm \ | |
| -v ${{ github.workspace }}:/root/keepkey-firmware:z \ | |
| ${{ env.BASE_IMAGE }} /bin/sh -c "\ | |
| mkdir /root/build && cd /root/build && \ | |
| cmake -C /root/keepkey-firmware/cmake/caches/device.cmake /root/keepkey-firmware \ | |
| -DCMAKE_BUILD_TYPE=MinSizeRel \ | |
| -DCMAKE_COLOR_MAKEFILE=ON && \ | |
| make && \ | |
| mkdir -p /root/keepkey-firmware/bin && \ | |
| cp bin/*.bin /root/keepkey-firmware/bin/ && \ | |
| cp bin/*.elf /root/keepkey-firmware/bin/ && \ | |
| chmod -R a+rw /root/keepkey-firmware/bin" | |
| - name: Rename firmware artifacts | |
| run: | | |
| cd bin | |
| for f in *.bin; do | |
| [ -f "$f" ] || continue | |
| mv "$f" "firmware.keepkey.v${{ steps.version.outputs.fw_version }}-${{ steps.version.outputs.git_short }}-${f}" | |
| done | |
| for f in *.elf; do | |
| [ -f "$f" ] || continue | |
| mv "$f" "firmware.keepkey.v${{ steps.version.outputs.fw_version }}-${{ steps.version.outputs.git_short }}-${f}" | |
| done | |
| ls -lh | |
| echo "::notice::Firmware v${{ steps.version.outputs.fw_version }} built successfully" | |
| - name: Upload firmware artifacts | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: firmware-v${{ steps.version.outputs.fw_version }}-${{ steps.version.outputs.git_short }} | |
| path: | | |
| bin/*.bin | |
| bin/*.elf | |
| retention-days: 90 | |
| # ═══════════════════════════════════════════════════════════ | |
| # STAGE 3: TEST — run only after builds succeed | |
| # ═══════════════════════════════════════════════════════════ | |
| unit-tests: | |
| needs: build-emulator | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Download emulator image | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: emu-image | |
| path: /tmp | |
| - name: Load emulator image | |
| run: docker load -i /tmp/emu-image.tar | |
| - name: Run unit tests | |
| run: | | |
| # make xunit returns non-zero if any test fails — capture | |
| # exit code so JUnit XML still gets copied for reporting | |
| docker run --rm \ | |
| -v ${{ github.workspace }}/test-reports:/kkemu/test-reports \ | |
| --entrypoint /bin/sh \ | |
| ${{ env.EMU_IMAGE }} \ | |
| -c "mkdir -p /kkemu/test-reports/firmware-unit && \ | |
| make xunit; RC=\$?; \ | |
| cp -r unittests/*.xml /kkemu/test-reports/firmware-unit/ 2>/dev/null; \ | |
| exit \$RC" | |
| - name: Upload unit test results | |
| uses: actions/upload-artifact@v7 | |
| if: always() | |
| with: | |
| name: unit-test-results | |
| path: test-reports/firmware-unit/ | |
| retention-days: 30 | |
| python-integration-tests: | |
| needs: [lint-format, static-analysis, check-submodules, secret-scan] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha || github.sha }} | |
| - name: Init submodules | |
| run: | | |
| git submodule update --init deps/crypto/trezor-firmware | |
| git submodule update --init deps/device-protocol | |
| git submodule update --init --recursive deps/python-keepkey | |
| git submodule update --init deps/googletest | |
| git submodule update --init deps/qrenc/QR-Code-generator | |
| git submodule update --init deps/sca-hardening/SecAESSTM32 | |
| - name: Build and run tests (docker compose) | |
| working-directory: scripts/emulator | |
| run: | | |
| # Run each test container — capture exit codes, always extract reports | |
| docker compose up --build --exit-code-from firmware-unit firmware-unit; FW_RC=$? | |
| docker compose up --build --exit-code-from python-keepkey python-keepkey; PY_RC=$? | |
| mkdir -p ${{ github.workspace }}/test-reports | |
| echo "=== Extracting test reports from Docker ===" | |
| PY_CONTAINER=$(docker compose ps -a -q python-keepkey) | |
| FW_CONTAINER=$(docker compose ps -a -q firmware-unit) | |
| docker cp "$FW_CONTAINER":/kkemu/test-reports/. ${{ github.workspace }}/test-reports/ || echo "WARN: firmware-unit docker cp failed" | |
| docker cp "$PY_CONTAINER":/kkemu/test-reports/. ${{ github.workspace }}/test-reports/ || echo "WARN: python-keepkey docker cp failed" | |
| echo "=== Extracted files ===" | |
| find ${{ github.workspace }}/test-reports -type f | head -30 | |
| echo "=== Screenshot PNGs ===" | |
| find ${{ github.workspace }}/test-reports/screenshots -name '*.png' 2>/dev/null | wc -l | |
| echo "PNGs on host" | |
| echo "firmware-unit exit code: $FW_RC" | |
| echo "python-keepkey exit code: $PY_RC" | |
| if [ "$FW_RC" -ne 0 ]; then | |
| echo "::error::firmware-unit tests failed (exit code $FW_RC)" | |
| fi | |
| if [ "$PY_RC" -ne 0 ]; then | |
| echo "::error::python-keepkey tests failed (exit code $PY_RC)" | |
| fi | |
| [ "$FW_RC" -eq 0 ] && [ "$PY_RC" -eq 0 ] || exit 1 | |
| - name: Upload Python test results | |
| uses: actions/upload-artifact@v7 | |
| if: always() | |
| with: | |
| name: python-test-results | |
| path: test-reports/python-keepkey/ | |
| retention-days: 30 | |
| - name: Upload OLED screenshots | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: oled-screenshots | |
| path: test-reports/screenshots/ | |
| retention-days: 90 | |
| if-no-files-found: warn | |
| - name: Tear down | |
| if: always() | |
| working-directory: scripts/emulator | |
| run: docker compose down -v || true | |
| # ═══════════════════════════════════════════════════════════ | |
| # STAGE 3b: TEST REPORT — generate PDF from test artifacts | |
| # ═══════════════════════════════════════════════════════════ | |
| generate-test-report: | |
| needs: [unit-tests, python-integration-tests] | |
| if: always() | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha || github.sha }} | |
| - name: Download unit test results | |
| uses: actions/download-artifact@v4 | |
| continue-on-error: true | |
| with: | |
| name: unit-test-results | |
| path: test-reports/firmware-unit/ | |
| - name: Download python test results | |
| uses: actions/download-artifact@v4 | |
| continue-on-error: true | |
| with: | |
| name: python-test-results | |
| path: test-reports/python-keepkey/ | |
| - name: Download OLED screenshots | |
| uses: actions/download-artifact@v4 | |
| continue-on-error: true | |
| with: | |
| name: oled-screenshots | |
| path: test-reports/screenshots/ | |
| - name: Extract firmware version | |
| id: version | |
| run: | | |
| FW_VERSION=$(sed -n '/^project/,/)/p' CMakeLists.txt | grep -oP '\d+\.\d+\.\d+' || echo "unknown") | |
| echo "fw_version=${FW_VERSION}" >> "$GITHUB_OUTPUT" | |
| - name: Init python-keepkey submodule | |
| run: git submodule update --init deps/python-keepkey | |
| - name: Generate test report PDF | |
| env: | |
| FW_VERSION: ${{ steps.version.outputs.fw_version }} | |
| run: python3 scripts/generate-test-report.py | |
| - name: Upload test report | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: test-report | |
| path: test-report.pdf | |
| retention-days: 90 | |
| # ═══════════════════════════════════════════════════════════ | |
| # STAGE 4: PUBLISH — manual trigger only, all tests must pass | |
| # ═══════════════════════════════════════════════════════════ | |
| publish-emulator: | |
| needs: [unit-tests, python-integration-tests, build-arm-firmware] | |
| if: >- | |
| github.event_name == 'workflow_dispatch' && | |
| github.event.inputs.publish_emulator == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha || github.sha }} | |
| - name: Download emulator image | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: emu-image | |
| path: /tmp | |
| - name: Load emulator image | |
| run: docker load -i /tmp/emu-image.tar | |
| - name: Extract firmware version | |
| id: version | |
| run: | | |
| FW_VERSION=$(sed -n '/^project/,/)/p' CMakeLists.txt | grep -oP '\d+\.\d+\.\d+') | |
| echo "fw_version=${FW_VERSION}" >> "$GITHUB_OUTPUT" | |
| echo "Firmware version: ${FW_VERSION}" | |
| - name: Tag images for publish | |
| run: | | |
| docker tag ${{ env.EMU_IMAGE }} kktech/kkemu:latest | |
| docker tag ${{ env.EMU_IMAGE }} kktech/kkemu:v${{ steps.version.outputs.fw_version }} | |
| - name: Login to DockerHub | |
| uses: docker/login-action@v4 | |
| with: | |
| username: ${{ secrets.KK_DOCKERHUB_USER }} | |
| password: ${{ secrets.KK_DOCKERHUB_PASS }} | |
| - name: Push emulator images | |
| run: | | |
| docker push kktech/kkemu:latest | |
| docker push kktech/kkemu:v${{ steps.version.outputs.fw_version }} |