1717 pull_request :
1818 branches : [master, develop, reconcile/upstream-sync]
1919
20+ # One run per ref: a new push supersedes the old instead of both burning a
21+ # runner to completion.
22+ concurrency :
23+ group : ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
24+ cancel-in-progress : true
25+
2026jobs :
2127 # ═══════════════════════════════════════════════════════════
2228 # STAGE 1: GATE
@@ -62,26 +68,74 @@ jobs:
6268 integration :
6369 needs : [lint]
6470 runs-on : ubuntu-latest
65- timeout-minutes : 30
71+ timeout-minutes : 15
6672
67- services :
68- kkemu :
69- image : kktech/kkemu:latest
70- ports :
71- - 11044:11044/udp
72- - 11045:11045/udp
73- - 5000:5000
73+ # NO published emulator image. This job BUILDS one from current firmware.
74+ #
75+ # It used to pull kktech/kkemu:latest -- a floating tag whose image was
76+ # five months and six minor versions stale. That single fact caused every
77+ # symptom we chased: 80 tests gating on requires_firmware("7.15.0") skipped
78+ # silently, and one unskipped test drove a ctime() path that segfaults on
79+ # the old image and does not exist in current firmware.
80+ #
81+ # Publishing a fresher image would only reset that clock. Building from
82+ # source removes the class: the emulator under test is, by construction,
83+ # the firmware the tests were written against.
7484
7585 steps :
7686 - uses : actions/checkout@v4
7787 with :
7888 submodules : recursive
89+ path : python-keepkey
90+
91+ # python-keepkey is a SUBMODULE of the firmware repo, so the firmware is
92+ # where the emulator lives. alpha is the fork's integration branch.
93+ - name : Checkout firmware
94+ uses : actions/checkout@v4
95+ with :
96+ repository : BitHighlander/keepkey-firmware
97+ ref : alpha
98+ path : keepkey-firmware
99+
100+ # NOT `submodules: recursive`. trezor-firmware carries a micropython
101+ # vendor tree whose lib/lwip lives on git.savannah.gnu.org, which serves
102+ # dumb HTTP and cannot do the shallow clone actions/checkout requests --
103+ # it fails the whole job. The firmware repo's own CI inits exactly these
104+ # paths, non-recursively, for the same reason.
105+ - name : Init the submodules the emulator build needs
106+ working-directory : keepkey-firmware
107+ run : |
108+ git submodule update --init --depth 1 deps/crypto/trezor-firmware
109+ git submodule update --init --depth 1 deps/device-protocol
110+ git submodule update --init --depth 1 deps/googletest
111+ git submodule update --init --depth 1 deps/qrenc/QR-Code-generator
112+ git submodule update --init --depth 1 deps/sca-hardening/SecAESSTM32
113+
114+ # Test THIS checkout of python-keepkey, not the one the firmware pins.
115+ - name : Overlay this python-keepkey onto the firmware tree
116+ run : |
117+ rm -rf keepkey-firmware/deps/python-keepkey
118+ cp -a python-keepkey keepkey-firmware/deps/python-keepkey
119+
120+ - name : Build the emulator
121+ timeout-minutes : 20
122+ working-directory : keepkey-firmware
123+ run : |
124+ docker build -t kkemu-ci -f scripts/emulator/Dockerfile .
125+
126+ - name : Start the emulator
127+ run : |
128+ docker run -d --name kkemu \
129+ -p 11044:11044/udp -p 11045:11045/udp -p 5000:5000 kkemu-ci
130+ sleep 3
131+ docker logs kkemu | head -5
79132
80133 - uses : actions/setup-python@v5
81134 with :
82135 python-version : ' 3.11'
83136
84137 - name : Install dependencies
138+ working-directory : python-keepkey
85139 run : |
86140 pip install --upgrade pip
87141 pip install "protobuf>=3.20,<4"
@@ -101,20 +155,67 @@ jobs:
101155 sleep 1
102156 done
103157
158+ # "The emulator answered a ping" is not "the emulator is the right
159+ # firmware". CI ran a 7.16-era suite against a 7.10.0 image for five
160+ # months: 80 tests gate on requires_firmware("7.15.0") and silently
161+ # SKIPPED, while one unskipped test drove a code path that segfaults in
162+ # 7.10.0 and is already fixed in 7.15 -- which reads as a product failure
163+ # but is only a stale image. A floating tag cannot tell you that. This
164+ # can, and it fails closed.
165+ - name : Assert the emulator is not older than the suite
166+ timeout-minutes : 2
167+ env :
168+ KK_TRANSPORT_MAIN : " 127.0.0.1:11044"
169+ KK_TRANSPORT_DEBUG : " 127.0.0.1:11045"
170+ KK_MIN_FW : " 7.15.0"
171+ KK_UDP_TIMEOUT : " 20"
172+ working-directory : keepkey-firmware/deps/python-keepkey/tests
173+ run : |
174+ python - <<'PY'
175+ import os, sys
176+ sys.path.insert(0, '..')
177+ import config
178+ from keepkeylib.client import KeepKeyDebuglinkClient
179+ c = KeepKeyDebuglinkClient(config.TRANSPORT(*config.TRANSPORT_ARGS,
180+ **config.TRANSPORT_KWARGS))
181+ c.set_debuglink(config.DEBUG_TRANSPORT(*config.DEBUG_TRANSPORT_ARGS,
182+ **config.DEBUG_TRANSPORT_KWARGS))
183+ c.init_device()
184+ f = c.features
185+ got = (f.major_version, f.minor_version, f.patch_version)
186+ floor = tuple(int(x) for x in os.environ['KK_MIN_FW'].split('.'))
187+ print('emulator firmware %d.%d.%d, floor %s' %
188+ (got + (os.environ['KK_MIN_FW'],)))
189+ if got < floor:
190+ sys.exit('FATAL: the emulator image predates the tests that run '
191+ 'against it. Republish kktech/kkemu from current '
192+ 'firmware and pin the new digest above.')
193+ PY
194+
195+ # Step-level timeout, deliberately: a JOB-level timeout ends the job as
196+ # "cancelled", which reads as an infra blip. A step timeout is a FAILURE.
104197 - name : Run integration tests
198+ timeout-minutes : 8
105199 env :
106200 KK_TRANSPORT_MAIN : " 127.0.0.1:11044"
107201 KK_TRANSPORT_DEBUG : " 127.0.0.1:11045"
108- PYTHONPATH : " ${{ github.workspace }}/keepkeylib:${{ github.workspace }}"
202+ PYTHONPATH : " ${{ github.workspace }}/keepkey-firmware/deps/python-keepkey"
203+ # A crashed emulator now raises instead of blocking in recv() forever.
204+ KK_UDP_TIMEOUT : " 45"
109205 run : |
110- cd tests
206+ # From the OVERLAID copy, not the standalone checkout: the
207+ # storage-version-gate tests assert against lib/firmware/storage.c,
208+ # which they find by walking UP. Run them as a sibling of the
209+ # firmware and they resolve; run them standalone and they fail
210+ # claiming the sources are missing.
211+ cd keepkey-firmware/deps/python-keepkey/tests
111212 pytest -v --junitxml=junit.xml 2>&1 | tee pytest-output.txt
112213 echo "${PIPESTATUS[0]}" > status
113214
114215 - name : Test summary
115216 if : always()
116217 run : |
117- XML="tests/junit.xml"
218+ XML="keepkey-firmware/deps/python-keepkey/ tests/junit.xml"
118219 echo "## 🔑 KeepKey python-keepkey — Integration Tests" >> "$GITHUB_STEP_SUMMARY"
119220 echo "" >> "$GITHUB_STEP_SUMMARY"
120221
@@ -161,15 +262,23 @@ jobs:
161262 echo "---" >> "$GITHUB_STEP_SUMMARY"
162263 echo "*KeepKey python-keepkey CI*" >> "$GITHUB_STEP_SUMMARY"
163264
164- - name : Upload test results
265+ # NO check_name. With one, this action publishes a SEPARATE check run
266+ # via the Checks API, and its require_tests default of 'false' means an
267+ # absent junit.xml -- which is exactly what a killed pytest leaves behind
268+ # -- reports conclusion:success with zero duration. That green check sat
269+ # on top of a job timing out at 30 minutes for at least six merges.
270+ # annotate_only keeps the inline annotations without minting a check.
271+ - name : Annotate test results
165272 uses : mikepenz/action-junit-report@v4
166273 if : always()
167274 with :
168- report_paths : tests/junit.xml
169- check_name : Integration Tests
275+ report_paths : keepkey-firmware/deps/python-keepkey/tests/junit.xml
276+ annotate_only : true
277+ require_tests : true
278+ fail_on_failure : true
170279
171280 - name : Fail on test failure
172281 if : always()
173282 run : |
174- STATUS=$(cat tests/status 2>/dev/null || echo "1")
283+ STATUS=$(cat keepkey-firmware/deps/python-keepkey/ tests/status 2>/dev/null || echo "1")
175284 [ "$STATUS" = "0" ] || exit 1
0 commit comments