Skip to content

Commit c429671

Browse files
authored
Merge pull request #170 from baba-dev/codex/add-preflight-build-and-boot-loop-guard
Add preflight automation for Tab5 builds
2 parents ce9e4cb + 81f25b6 commit c429671

6 files changed

Lines changed: 463 additions & 0 deletions

File tree

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
name: IDF Preflight (esp32p4)
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
jobs:
8+
build:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- name: Checkout
12+
uses: actions/checkout@v4
13+
14+
# Install Python deps (pytest-embedded optional; enables on-target tests when HW is present)
15+
- name: Python setup
16+
uses: actions/setup-python@v5
17+
with:
18+
python-version: "3.11"
19+
- run: |
20+
python -m pip install --upgrade pip
21+
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
22+
pip install pyserial pytest pytest-embedded pytest-embedded-idf
23+
shell: bash
24+
25+
# Install ESP-IDF using official action (respects Linux runners)
26+
- name: Install ESP-IDF
27+
uses: espressif/install-esp-idf-action@v1
28+
with:
29+
version: latest
30+
# Alternatively, to match your repo script exactly, source your installer:
31+
# - run: bash tools/install_idf.sh
32+
33+
- name: Preflight
34+
shell: bash
35+
run: |
36+
bash scripts/preflight.sh
37+
38+
# Upload build artifacts & logs for debugging
39+
- name: Archive build
40+
if: always()
41+
uses: actions/upload-artifact@v4
42+
with:
43+
name: tab5-build
44+
path: |
45+
platforms/tab5/build/**
46+
**/build/tests/**
47+
if-no-files-found: ignore

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Run these from the repository root unless noted otherwise.
2121
| --- | --- | --- |
2222
| `idf.py set-target esp32p4` | First build in a fresh checkout | Only needed once per workspace unless you clean the build folder.
2323
| `idf.py build` | Every PR | Must succeed; catches most integration issues.
24+
| `bash scripts/preflight.sh` | Before requesting review | Runs repo installer, fetches deps, builds Tab5, runs tests, and checks for boot loops when SERIAL_PORT is set. |
2425
| `idf.py clang-format` <br>or `clang-format -style=file -i <files>` | Whenever C/C++ is touched | Uses the repo's `.clang-format` (clang-format 13). Stage formatted files.
2526
| `idf.py lint` | When modifying components or build metadata | Ensures `idf_component.yml` metadata stays valid; safe to run even if no changes were made.
2627
| `ctags -R` / IDE index refresh | Optional but recommended | Keeps symbol navigation accurate after large refactors.
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
# Task: Preflight build, tests, and boot-loop guard for M5Tab5-UserDemo
2+
3+
## Objective
4+
When Codex (or any contributor) proposes code:
5+
1) Use repo tools (esp. `tools/install_idf.sh`) to prepare ESP-IDF.
6+
2) Build exactly like the maintainer does.
7+
3) Run host/unit tests in `tests/` and fail if any test fails.
8+
4) If a serial port is available, run a boot-loop guard immediately after flashing on a local/dev machine (CI step is optional unless a hardware runner is present).
9+
5) Only open PRs that **pass preflight**.
10+
11+
## Canonical local build sequence (MUST be followed)
12+
```bash
13+
git clone https://github.com/baba-dev/M5Tab5-UserDemo
14+
cd M5Tab5-UserDemo
15+
16+
# Install / activate IDF via our repo tool (no ad-hoc installers)
17+
bash tools/install_idf.sh # sets/exports IDF env for this shell
18+
19+
# Fetch external deps (must succeed)
20+
python ./fetch_repos.py
21+
22+
# Build for Tab5
23+
cd platforms/tab5
24+
idf.py set-target esp32p4
25+
idf.py build
26+
```
27+
28+
Use idf.py set-target esp32p4 (this clears & regenerates sdkconfig as per IDF docs).
29+
Espressif Systems
30+
31+
Don’t invent alternative flows or other actions to “speed up”; this path is authoritative. The M5Stack Tab5 user demo guide also shows the same platforms/tab5 layout and fetch_repos.py.
32+
M5Stack Docs
33+
34+
Tests (MUST PASS)
35+
36+
Run component/unit tests (Unity) discovered under tests/ or component test/ folders. Unity test layout is the official way for ESP-IDF.
37+
Espressif Systems
38+
39+
If a host CMake test project exists, build it and run ctest with --output-on-failure.
40+
41+
On hardware test rigs (optional), use pytest-embedded to drive simple on-target checks.
42+
Espressif Systems
43+
+1
44+
45+
Boot-loop guard (MUST if serial available; optional in CI)
46+
47+
After flashing on a developer machine, run scripts/bootloop_guard.py with --port <PORT> for 30–45s.
48+
49+
Fail if monitor shows repeating reset banners/panics (e.g., rst:, panic, abort, watchdog), or >3 resets in the window. Panic/cores are standard IDF behaviors.
50+
Espressif Systems
51+
+2
52+
Espressif Systems
53+
+2
54+
55+
Acceptance criteria (PRs that Codex proposes MUST meet all)
56+
57+
Builds with idf.py set-target esp32p4 && idf.py build under platforms/tab5 with the IDF env from tools/install_idf.sh.
58+
Espressif Systems
59+
60+
python ./fetch_repos.py completes without errors (deps present).
61+
62+
./scripts/preflight.sh exits 0 locally; CI job idf-preflight is green.
63+
64+
If SERIAL_PORT is set or --port provided, bootloop_guard.py reports NO_BOOTLOOP.
65+
66+
All tests in tests/ (and component test/) pass (Unity/pytest).
67+
Espressif Systems
68+
+1
69+
70+
Notes
71+
72+
Use IDF monitor semantics and patterns for parsing the serial stream (we rely on standard IDF monitor output).
73+
Espressif Systems
74+
75+
Do not downgrade/replace IDF. Use our installer script and the IDF it selects.
76+
77+
78+
---
79+
80+
# Add these helper files
81+
82+
## 1) `scripts/preflight.sh` (bash)
83+
84+
```bash
85+
#!/usr/bin/env bash
86+
set -euo pipefail
87+
88+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
89+
cd "$ROOT_DIR"
90+
91+
echo "[preflight] Using repo installer to setup ESP-IDF..."
92+
bash tools/install_idf.sh
93+
94+
echo "[preflight] Fetching external components..."
95+
python ./fetch_repos.py
96+
97+
echo "[preflight] Building Tab5 app (esp32p4)..."
98+
pushd platforms/tab5 >/dev/null
99+
idf.py set-target esp32p4
100+
idf.py build
101+
popd >/dev/null
102+
103+
echo "[preflight] Running host/unit tests if present..."
104+
if [ -f tests/CMakeLists.txt ]; then
105+
cmake -S tests -B build/tests -DCMAKE_BUILD_TYPE=Debug
106+
cmake --build build/tests -- -j
107+
ctest --test-dir build/tests --output-on-failure
108+
fi
109+
110+
# Component Unity tests via IDF (if any component has test/ with Unity cases)
111+
if grep -R --include="CMakeLists.txt" -n "idf_component_register" components >/dev/null 2>&1; then
112+
echo "[preflight] Scanning for Unity tests in components/*/test ..."
113+
# IDF standard: tests usually land in a dedicated test app; if one exists, build it.
114+
# If you have a test app target (e.g., 'unit_test_app'), you can uncomment:
115+
# pushd platforms/tab5 >/dev/null
116+
# idf.py build -DIDF_UNIT_TESTING=1
117+
# popd >/dev/null
118+
fi
119+
120+
# Optional hardware boot-loop guard
121+
if [ "${SERIAL_PORT:-}" != "" ]; then
122+
echo "[preflight] Serial port detected ($SERIAL_PORT); running boot-loop guard..."
123+
python scripts/bootloop_guard.py --port "$SERIAL_PORT" --seconds 45 || {
124+
echo "::error::Boot-loop or panic detected on device"
125+
exit 1
126+
}
127+
fi
128+
129+
echo "[preflight] OK"
130+
```
131+
132+
Why these choices?
133+
134+
idf.py is the canonical front-end for build/flash/monitor.
135+
Espressif Systems
136+
137+
set-target explicitly selects esp32p4 and regenerates config as documented.
138+
Espressif Systems
139+
140+
## 2) scripts/bootloop_guard.py
141+
```
142+
#!/usr/bin/env python3
143+
import argparse, sys, time, re
144+
try:
145+
import serial
146+
except ImportError:
147+
print("pyserial not installed. pip install pyserial", file=sys.stderr)
148+
sys.exit(2)
149+
150+
PATTERNS = [
151+
re.compile(rb"rst:0x", re.I),
152+
re.compile(rb"panic", re.I),
153+
re.compile(rb"abort", re.I),
154+
re.compile(rb"Guru Meditation", re.I), # legacy wording still appears on some targets
155+
re.compile(rb"watchdog", re.I),
156+
]
157+
158+
def main():
159+
ap = argparse.ArgumentParser()
160+
ap.add_argument("--port", required=True, help="Serial port (e.g. COM5 or /dev/ttyACM0)")
161+
ap.add_argument("--baud", type=int, default=115200)
162+
ap.add_argument("--seconds", type=int, default=30)
163+
args = ap.parse_args()
164+
165+
resets = 0
166+
start = time.time()
167+
168+
try:
169+
with serial.Serial(args.port, args.baud, timeout=0.2) as ser:
170+
ser.reset_input_buffer()
171+
while time.time() - start < args.seconds:
172+
data = ser.read(8192)
173+
if not data:
174+
continue
175+
sys.stdout.buffer.write(data)
176+
sys.stdout.flush()
177+
for pat in PATTERNS:
178+
if pat.search(data):
179+
if b"rst:" in data:
180+
resets += 1
181+
else:
182+
print("\n[bootloop_guard] panic/abort/watchdog matched -> FAIL", file=sys.stderr)
183+
return 1
184+
if resets > 3:
185+
print("\n[bootloop_guard] too many resets -> FAIL", file=sys.stderr)
186+
return 1
187+
except serial.SerialException as e:
188+
print(f"[bootloop_guard] Serial error: {e}", file=sys.stderr)
189+
return 2
190+
191+
print("\n[bootloop_guard] NO_BOOTLOOP", file=sys.stderr)
192+
return 0
193+
194+
if __name__ == "__main__":
195+
sys.exit(main())
196+
```
197+
198+
This follows IDF monitor semantics (scan reset banner/panic strings). For reference: idf.py monitor behavior & fatal error/panic docs.
199+
Espressif Systems
200+
+1
201+
202+
Make it executable:
203+
204+
git update-index --add --chmod=+x scripts/preflight.sh
205+
206+
CI that enforces the same thing
207+
208+
Path: .github/workflows/idf-preflight.yml
209+
210+
```
211+
name: IDF Preflight (esp32p4)
212+
213+
on:
214+
push:
215+
pull_request:
216+
217+
jobs:
218+
build:
219+
runs-on: ubuntu-latest
220+
steps:
221+
- name: Checkout
222+
uses: actions/checkout@v4
223+
224+
# Install Python deps (pytest-embedded optional; enables on-target tests when HW is present)
225+
- name: Python setup
226+
uses: actions/setup-python@v5
227+
with:
228+
python-version: "3.11"
229+
- run: |
230+
python -m pip install --upgrade pip
231+
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
232+
pip install pyserial pytest pytest-embedded pytest-embedded-idf
233+
shell: bash
234+
235+
# Install ESP-IDF using official action (respects Linux runners)
236+
- name: Install ESP-IDF
237+
uses: espressif/install-esp-idf-action@v1
238+
with:
239+
version: latest
240+
# Alternatively, to match your repo script exactly, source your installer:
241+
# - run: bash tools/install_idf.sh
242+
243+
- name: Preflight
244+
shell: bash
245+
run: |
246+
bash scripts/preflight.sh
247+
248+
# Upload build artifacts & logs for debugging
249+
- name: Archive build
250+
if: always()
251+
uses: actions/upload-artifact@v4
252+
with:
253+
name: tab5-build
254+
path: |
255+
platforms/tab5/build/**
256+
**/build/tests/**
257+
if-no-files-found: ignore
258+
```
259+
260+
Uses Espressif’s official IDF setup/CI actions — they wrap the official IDF images/tools and are the recommended way to build in GitHub Actions.
261+
GitHub
262+
+2
263+
GitHub
264+
+2
265+
266+
Your preflight script runs the same sequence as you do locally; that parity is the point.
267+
268+
If you later add a self-hosted runner with the Tab5 attached, set SERIAL_PORT in runner env so the boot-loop guard runs automatically and gates the PR.
269+
270+
Why this approach is solid
271+
272+
Exact target selection & build: idf.py set-target esp32p4 is the supported way, and it (re)generates sdkconfig per target.
273+
Espressif Systems
274+
275+
Unity tests are first-class in IDF; placing tests in test/ with unity.h is canonical.
276+
Espressif Systems
277+
278+
Boot-loop detection: leveraging known monitor strings (resets, panics, watchdogs) mirrors idf.py monitor output and IDF’s panic handler behavior.
279+
Espressif Systems
280+
+1
281+
282+
Repo-specific layout: Your Tab5 demo uses platforms/tab5 and fetch_repos.py, exactly what we enforce.
283+
M5Stack Docs
284+
285+
Optional (nice to have, later)
286+
287+
On-target automated tests: Add a tiny pytest file under tests/embedded/ that flashes and asserts a boot banner + single UI tick using pytest-embedded-idf. This follows Espressif’s guidance for automated target testing.
288+
Espressif Systems
289+
290+
CI matrix for multiple IDF versions (e.g., v5.4.x, latest) using install-esp-idf-action.
291+
GitHub
292+
293+
Use esp-idf-ci-action if you prefer the Docker-wrapped builder.
294+
GitHub
295+
296+
What I created for you (summary)
297+
298+
ai/tasks/00_preflight_build_and_bootloop_guard.md — the Codex-visible checklist (first file Codex will read).
299+
300+
scripts/preflight.sh — single entry-point that replicates your local workflow, runs tests, and (optionally) boot-loop check.
301+
302+
scripts/bootloop_guard.py — simple serial log scanner to catch resets/panics.
303+
304+
.github/workflows/idf-preflight.yml — CI to enforce the preflight on every PR/push.
305+
306+
If you want, I can also add a one-line pointer in docs/AI_GUIDELINES.md:
307+
308+
“AI contributors must pass scripts/preflight.sh locally before opening PRs.”
309+
310+
That’s it — from here, Codex has no excuse to ship untested, non-building changes. ✅

docs/AI_GUIDELINES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ safety rails, and formatting rules for automated contributions.
3131

3232
- Run `idf.py build` before finalizing a change. Resolve compiler warnings
3333
rather than silencing them.
34+
- Run `bash scripts/preflight.sh` and ensure it exits 0 before opening a PR.
3435
- Execute `idf.py lint` when modifying integration metadata or component
3536
manifests.
3637
- Run `npx markdownlint docs` after editing Markdown. Fix or justify any

0 commit comments

Comments
 (0)