Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
86e5fef
release: bump version to 7.14.0
BitHighlander Mar 18, 2026
cbba7e5
feat(ci): add screen-zoo artifact — OLED screenshots per CI run
BitHighlander Mar 19, 2026
954fe67
fix(ci): patch client.py at runtime instead of modifying submodule
BitHighlander Mar 19, 2026
b86f32f
fix(ci): revert python-keepkey submodule to upstream commit
BitHighlander Mar 19, 2026
bf5a285
fix(ci): install pytest/Pillow in zoo, run emu+zoo in single container
BitHighlander Mar 19, 2026
be1966a
fix(ci): bake Pillow+pytest into emu Docker image, drop 887MB artifact
BitHighlander Mar 19, 2026
f1c7b60
fix(ci): use python3 not python for pytest in zoo script
BitHighlander Mar 19, 2026
30fe741
fix(ci): add SCREENSHOT verification + debug output to zoo script
BitHighlander Mar 19, 2026
d4a0827
fix(ci): simplify zoo — one-line sed patch, no pip install needed
BitHighlander Mar 19, 2026
fbcb763
fix(ci): wrap PIL import in try/except so missing Pillow doesn't cras…
BitHighlander Mar 19, 2026
37e1a5f
fix(ci): only trigger push on master/develop — PRs cover feature bran…
BitHighlander Mar 19, 2026
852a874
fix(ci): install Pillow via apk (Alpine base), fail if zoo is empty
BitHighlander Mar 19, 2026
f9d181a
fix(ci): guarantee zoo artifact with manifest.txt, show full pytest o…
BitHighlander Mar 19, 2026
239afcd
fix(ci): use heredoc for client.py patch — fixes IndentationError
BitHighlander Mar 19, 2026
2c2b00e
fix(ci): ry/8 -> ry//8 — Python 3 integer division in screenshot code
BitHighlander Mar 19, 2026
65564d1
fix(ci): wrap screenshot capture in try/except — handles layout size …
BitHighlander Mar 19, 2026
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
100 changes: 60 additions & 40 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ name: CI

on:
push:
branches: [master, develop, 'feature/**', 'fix/**', 'release/**', 'hotfix/**']
branches: [master, develop]
pull_request:
branches: [master, develop]
workflow_dispatch:
Expand Down Expand Up @@ -204,10 +204,10 @@ jobs:
# STAGE 2: BUILD — compile only after gate passes
# ═══════════════════════════════════════════════════════════

build-emulator:
build-and-test-emulator:
needs: [check-submodules, secret-scan]
runs-on: ubuntu-latest
timeout-minutes: 15
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v4
Expand Down Expand Up @@ -250,10 +250,61 @@ jobs:
-f scripts/emulator/Dockerfile \
.

- name: Save emulator image
- name: Run unit tests
run: |
mkdir -p test-reports/firmware-unit
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: Generate screen zoo
if: always()
run: |
mkdir -p screen-zoo
docker run --rm \
-v ${{ github.workspace }}/screen-zoo:/kkemu/screen-zoo:rw \
-v ${{ github.workspace }}/scripts/emulator/screen-zoo.sh:/kkemu/scripts/emulator/screen-zoo.sh:ro \
-e KEEPKEY_SCREENSHOT=1 \
-e KK_TRANSPORT_MAIN=127.0.0.1:11044 \
-e KK_TRANSPORT_DEBUG=127.0.0.1:11045 \
${{ env.EMU_IMAGE }} \
/bin/sh -c "\
python3 ./scripts/emulator/bridge.py & \
./bin/kkemu & \
sleep 2 && \
/bin/sh /kkemu/scripts/emulator/screen-zoo.sh"

- name: Upload unit test results
uses: actions/upload-artifact@v4
if: always()
with:
name: unit-test-results
path: test-reports/firmware-unit/
retention-days: 30

- name: Upload screen zoo
uses: actions/upload-artifact@v4
if: always()
with:
name: screen-zoo
path: screen-zoo/
retention-days: 90

- name: Save emulator image (publish only)
if: >-
github.event_name == 'workflow_dispatch' &&
github.event.inputs.publish_emulator == 'true'
run: docker save ${{ env.EMU_IMAGE }} -o /tmp/emu-image.tar

- name: Upload emulator image artifact
- name: Upload emulator image (publish only)
if: >-
github.event_name == 'workflow_dispatch' &&
github.event.inputs.publish_emulator == 'true'
uses: actions/upload-artifact@v4
with:
name: emu-image
Expand Down Expand Up @@ -432,40 +483,7 @@ jobs:
# 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@v4
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@v4
if: always()
with:
name: unit-test-results
path: test-reports/firmware-unit/
retention-days: 30
# unit-tests and screen-zoo are now part of build-and-test-emulator above

python-integration-tests:
needs: [check-submodules, secret-scan]
Expand Down Expand Up @@ -515,12 +533,14 @@ jobs:
working-directory: scripts/emulator
run: docker compose down -v || true

# screen-zoo is now part of build-and-test-emulator above

# ═══════════════════════════════════════════════════════════
# STAGE 4: PUBLISH — manual trigger only, all tests must pass
# ═══════════════════════════════════════════════════════════

publish-emulator:
needs: [unit-tests, python-integration-tests, build-arm-firmware, build-arm-firmware-btc-only]
needs: [build-and-test-emulator, python-integration-tests, build-arm-firmware, build-arm-firmware-btc-only]
if: >-
github.event_name == 'workflow_dispatch' &&
github.event.inputs.publish_emulator == 'true'
Expand Down
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.7.2)

project(
KeepKeyFirmware
VERSION 7.10.0
VERSION 7.14.0
LANGUAGES C CXX ASM)

set(BOOTLOADER_MAJOR_VERSION 2)
Expand Down
6 changes: 6 additions & 0 deletions scripts/emulator/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ RUN cmake -C ./cmake/caches/emulator.cmake . \

RUN make -j

# Screen zoo: Pillow for OLED capture (pytest already in base image)
# Base image is Alpine-based, so use apk for native deps then pip for Pillow
RUN apk add --no-cache py3-pillow 2>/dev/null || \
(apk add --no-cache python3-dev zlib-dev jpeg-dev gcc musl-dev && pip3 install Pillow) 2>/dev/null || \
echo "WARNING: Pillow not installed — screen zoo will be empty"

EXPOSE 11044/udp 11045/udp
EXPOSE 5000
CMD ["/kkemu/scripts/emulator/run.sh"]
Expand Down
108 changes: 108 additions & 0 deletions scripts/emulator/screen-zoo.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
#!/bin/sh
# Generate device screen zoo — captures OLED framebuffer PNGs from targeted
# test flows against the emulator via the debug link.
#
# Outputs: /kkemu/screen-zoo/<chain>/*.png

set -e

# Patch client.py to enable screenshots when KEEPKEY_SCREENSHOT=1.
# The original has SCREENSHOT = False hardcoded. We replace it in-place.
CLIENT_PY="/kkemu/deps/python-keepkey/keepkeylib/client.py"
if grep -q "^SCREENSHOT = False" "$CLIENT_PY" 2>/dev/null; then
# Replace the hardcoded False with a try/except block
python3 << 'PYEOF'
with open("/kkemu/deps/python-keepkey/keepkeylib/client.py") as f:
lines = f.readlines()
with open("/kkemu/deps/python-keepkey/keepkeylib/client.py", "w") as f:
for line in lines:
if line.strip() == "SCREENSHOT = False":
f.write("try:\n")
f.write(" from PIL import Image\n")
f.write(" SCREENSHOT = True\n")
f.write("except ImportError:\n")
f.write(" SCREENSHOT = False\n")
else:
f.write(line)
PYEOF
echo "[screen-zoo] Patched client.py"
# Fix Python 2 integer division + wrap in try/except for robustness
python3 << 'PYEOF2'
with open("/kkemu/deps/python-keepkey/keepkeylib/client.py") as f:
c = f.read()
# Fix integer division
c = c.replace("ry / 8", "ry // 8")
# Wrap the screenshot block in try/except so layout issues don't crash tests
c = c.replace(
" if SCREENSHOT and self.debug:\n layout = self.debug.read_layout()",
" if SCREENSHOT and self.debug:\n try:\n layout = self.debug.read_layout()"
)
c = c.replace(
" self.screenshot_id += 1\n\n resp = super(DebugLinkMixin, self).call_raw(msg)",
" self.screenshot_id += 1\n except Exception as _e:\n pass\n\n resp = super(DebugLinkMixin, self).call_raw(msg)"
)
with open("/kkemu/deps/python-keepkey/keepkeylib/client.py", "w") as f:
f.write(c)
PYEOF2
echo "[screen-zoo] Patched integer division + exception guard"
python3 -c "from PIL import Image; print('[screen-zoo] Pillow OK')" 2>&1 || echo "[screen-zoo] WARNING: Pillow not available"
else
echo "[screen-zoo] client.py already patched or not found"
fi

cd /kkemu/deps/python-keepkey/tests

# Write a manifest so the artifact always has at least one file
echo "screen-zoo generated at $(date -u)" > /kkemu/screen-zoo/manifest.txt

run_zoo() {
chain="$1"
test_file="$2"
out_dir="/kkemu/screen-zoo/${chain}"

mkdir -p "${out_dir}"
echo "=== ${chain} ==="

if [ ! -f "${test_file}" ]; then
echo " SKIP (not found)"
return
fi

# pytest runs from tests/ dir, imports keepkeylib via sys.path=['../']
# Full output so we can debug collection errors
python3 -m pytest -x -v "${test_file}" 2>&1 || true

# Collect screenshots (written to cwd by call_raw)
mv scr*.png "${out_dir}/" 2>/dev/null || true
COUNT=$(ls "${out_dir}"/*.png 2>/dev/null | wc -l | tr -d ' ')
echo " -> ${COUNT} screens"
}

# ── Zcash ──────────────────────────────────────────
run_zoo "zcash-orchard-fvk" "test_msg_zcash_orchard.py"
run_zoo "zcash-orchard-pczt" "test_msg_zcash_sign_pczt.py"
run_zoo "zcash-transparent" "test_msg_signtx_zcash.py"

# ── Solana ─────────────────────────────────────────
run_zoo "solana" "test_msg_solana_getaddress.py"

# ── Ethereum / EVM ─────────────────────────────────
run_zoo "ethereum-address" "test_msg_ethereum_getaddress.py"
run_zoo "ethereum-sign" "test_msg_ethereum_signtx.py"
run_zoo "ethereum-xfer" "test_msg_ethereum_signtx_xfer.py"
run_zoo "ethereum-erc20" "test_msg_ethereum_erc20_approve.py"
run_zoo "ethereum-message" "test_msg_ethereum_message.py"

# ── Summary ────────────────────────────────────────
echo ""
echo "=== Screen Zoo Summary ==="
TOTAL=0
for dir in /kkemu/screen-zoo/*/; do
[ -d "$dir" ] || continue
chain=$(basename "$dir")
count=$(ls "$dir"/*.png 2>/dev/null | wc -l | tr -d ' ')
TOTAL=$((TOTAL + count))
printf " %-30s %d screens\n" "$chain" "$count"
done
echo " ------------------------------------"
printf " %-30s %d screens\n" "TOTAL" "$TOTAL"
Loading