gitea/runner-images:ubuntu-latest has no cmake; test job exited 127 before firmware matrix could run.
255 lines
10 KiB
Markdown
255 lines
10 KiB
Markdown
# CI/CD — Gitea Actions
|
||
|
||
Status: planned on `refactor/esp-idf-modular-ui`. Implementation follows the IDF skeleton
|
||
(migration step 2 in `docs/TARGET_ARCHITECTURE.md`). Workflows live in **`.gitea/workflows/`**
|
||
(Gitea Actions, GitHub-compatible YAML). PRs target **`develop`**.
|
||
|
||
## Purpose
|
||
|
||
Every change that lands on `develop` must:
|
||
|
||
1. Run **host unit tests** for recipe tables and the machine state machine (no hardware).
|
||
2. **Build two firmware images**: `board_wroom` (ESP32 classic) and `board_jc4827w543` (ESP32-S3).
|
||
3. Publish flashable artifacts (app binary, bootloader, partition table, merged image, size report).
|
||
|
||
Legacy PlatformIO (`platformio.ini`) is not a CI target.
|
||
|
||
## Runner requirements
|
||
|
||
Gitea `act_runner` must be registered against `git.i3omb.com` with labels that match `runs-on`.
|
||
|
||
| Workflow `runs-on` | Typical runner label | Notes |
|
||
| --- | --- | --- |
|
||
| `ubuntu-latest` | `ubuntu-latest:docker://gitea/runner-images:ubuntu-latest` | Host tests, checkout, artifact upload. Image has **no cmake/gcc** — the test job must `apt-get install` them. |
|
||
| (job `container`) | Docker socket available on the runner | Firmware jobs use `espressif/idf:<tag>` as the job container |
|
||
|
||
Firmware builds need:
|
||
|
||
- Docker on the runner (job-level `container:`)
|
||
- Enough disk for the IDF image (~several GB) and two `build/` trees
|
||
- Outbound pull of `docker.io/espressif/idf` (or a mirror on the Gitea registry)
|
||
|
||
If the instance cannot pull Docker Hub, mirror `espressif/idf:v5.4` into the Gitea container registry and point `container.image` at that.
|
||
|
||
Do not depend on `espressif/esp-idf-ci-action` or other GitHub-hosted actions for the compile step. Those often assume GitHub path layout. Drive `idf.py` inside `espressif/idf` instead. `actions/checkout` and `actions/upload-artifact` (or Gitea’s equivalents) are fine.
|
||
|
||
## Triggers
|
||
|
||
| Event | What runs |
|
||
| --- | --- |
|
||
| `pull_request` into `develop` | tests + both firmware builds (required check) |
|
||
| `push` to `develop` | tests + both firmware builds; keep artifacts |
|
||
| `push` to `refactor/**` and `feature/**` | same, so the branch is green before the PR |
|
||
| `push` tags `v*` | tests + builds + attach binaries to the Gitea release |
|
||
| `workflow_dispatch` | manual rebuild |
|
||
|
||
`main` is not a CI integration branch for this refactor.
|
||
|
||
## Jobs
|
||
|
||
```
|
||
test (host) ──► firmware [wroom / jc4827w543] ──► (tag only) release
|
||
```
|
||
|
||
Firmware must `needs: test`. A failing recipe or state-machine test must not produce a binary.
|
||
|
||
### 1. `test` — host unit tests
|
||
|
||
Runs on `ubuntu-latest` **without** the IDF container so the gate is fast.
|
||
|
||
- Toolchain: CMake + GCC (C11), CTest.
|
||
- Tree: `tests/host/` linking `components/app_process` and `components/app_machine` against **stub HAL** (`tests/host/stubs/`: motor, temp, audio, display no-ops; queues replaced with a tiny fake or a vendored FreeRTOS POSIX port if the machine task is exercised).
|
||
- Framework: Unity (can be vendored) or plain `assert` + CTest. Prefer Unity so on-target tests later share the same style.
|
||
- Must cover:
|
||
- `find` / lookup of all six process names
|
||
- frozen default times, temps, CW/CCW for every step (values from `docs/CURRENT_STATE.md`)
|
||
- session ±5 s overlay does not mutate the const table
|
||
- state machine: Idle → Select → Arm → Start → Stop (EN disabled) → Resume → Complete → Return
|
||
- `Stop` is accepted in `StepRunning` and during a complete-alarm
|
||
- Recipe-value tests fail the build if defaults change without an explicit doc update.
|
||
|
||
Command sketch:
|
||
|
||
```bash
|
||
cmake -S tests/host -B build/host
|
||
cmake --build build/host
|
||
ctest --test-dir build/host --output-on-failure
|
||
```
|
||
|
||
Until `tests/host` exists, the workflow file is not merged as a required check — or the job is `if`’d on path existence. Do not ship a red pipeline on docs-only commits after the workflow is enabled; use:
|
||
|
||
```yaml
|
||
- name: Host tests
|
||
if: hashFiles('tests/host/CMakeLists.txt') != ''
|
||
```
|
||
|
||
### 2. `firmware` — matrix build
|
||
|
||
Job container: `espressif/idf:v5.4` (pin the tag; bump in one place when IDF is upgraded).
|
||
|
||
Matrix:
|
||
|
||
| Name | `IDF_TARGET` | Board cmake/sdkconfig |
|
||
| --- | --- | --- |
|
||
| `wroom` | `esp32` | `-DAUTOFILM_BOARD=wroom` + `sdkconfig.defaults` + `sdkconfig.wroom` |
|
||
| `jc4827w543` | `esp32s3` | `-DAUTOFILM_BOARD=jc4827w543` + `sdkconfig.defaults` + `sdkconfig.s3` |
|
||
|
||
Steps:
|
||
|
||
1. Checkout (full history not required).
|
||
2. `source /opt/esp/idf/export.sh` (already true in the official image entrypoint; use `idf.py` directly).
|
||
3. `idf.py set-target ${IDF_TARGET}` then
|
||
`idf.py @sdkconfig.defaults @sdkconfig.<board> -DAUTOFILM_BOARD=<board> build`
|
||
4. `idf.py size` (fail the job only if the binary will not fit flash; log the report either way).
|
||
5. Merge a single flash image:
|
||
`esptool.py --chip ${IDF_TARGET} merge_bin -o autofilm-${board}-${GIT_SHA}.bin @build/flash_args`
|
||
6. Upload artifacts (see below).
|
||
|
||
S3 build must enable octal PSRAM in `sdkconfig.s3`. WROOM must not.
|
||
|
||
Arduino-as-component is pulled by the IDF CMake on the WROOM image first; the S3 image may share it until debt burn-down. Cache `managed_components/` by lock hash when a `idf_component.yml` exists.
|
||
|
||
### 3. `release` (tags only)
|
||
|
||
`needs: [test, firmware]`. Download both artifact sets and attach to the Gitea release for tag `v*`:
|
||
|
||
- `autofilm-wroom-<version>.bin` (merged)
|
||
- `autofilm-jc4827w543-<version>.bin` (merged)
|
||
- raw `app.bin` / `bootloader.bin` / `partition-table.bin` per board
|
||
- `sdkconfig` snapshot per board
|
||
- `size.txt` per board
|
||
|
||
Version string is the git tag. CI also stamps `version` in firmware via `-DAUTOFILM_GIT_DESC=$(git describe --always --dirty)`.
|
||
|
||
## Artifacts
|
||
|
||
| File | Why |
|
||
| --- | --- |
|
||
| `autofilm-<board>-<sha>.bin` | Merged image for `esptool write_flash 0x0` |
|
||
| `build/autofilm.bin` (app) | OTA / partial flash |
|
||
| `build/bootloader/bootloader.bin` | First-time flash |
|
||
| `build/partition_table/partition-table.bin` | First-time flash |
|
||
| `build/flasher_args.json` | Exact offsets |
|
||
| `size.txt` / `size.json` | Flash/RAM budget |
|
||
| `sdkconfig` | Repro the build flags |
|
||
|
||
Retention: 14 days on branch builds, keep release attachments. Artifact names must include board + short SHA so WROOM and S3 cannot overwrite each other.
|
||
|
||
## Example workflow (target shape)
|
||
|
||
Path: `.gitea/workflows/ci.yml`
|
||
|
||
```yaml
|
||
name: ci
|
||
|
||
on:
|
||
pull_request:
|
||
branches: [develop]
|
||
push:
|
||
branches: [develop, "refactor/**", "feature/**"]
|
||
tags: ["v*"]
|
||
workflow_dispatch:
|
||
|
||
jobs:
|
||
test:
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
- name: Install host toolchain
|
||
if: hashFiles('tests/host/CMakeLists.txt') != ''
|
||
run: |
|
||
export DEBIAN_FRONTEND=noninteractive
|
||
if command -v sudo >/dev/null 2>&1 && [ "$(id -u)" -ne 0 ]; then
|
||
SUDO=sudo
|
||
else
|
||
SUDO=
|
||
fi
|
||
$SUDO apt-get update
|
||
$SUDO apt-get install -y --no-install-recommends cmake gcc g++ make
|
||
- name: Host unit tests
|
||
if: hashFiles('tests/host/CMakeLists.txt') != ''
|
||
run: |
|
||
cmake -S tests/host -B build/host
|
||
cmake --build build/host
|
||
ctest --test-dir build/host --output-on-failure
|
||
|
||
firmware:
|
||
needs: test
|
||
runs-on: ubuntu-latest
|
||
container:
|
||
image: docker.io/espressif/idf:v5.4
|
||
strategy:
|
||
fail-fast: false
|
||
matrix:
|
||
include:
|
||
- board: wroom
|
||
target: esp32
|
||
sdkconfig: sdkconfig.wroom
|
||
- board: jc4827w543
|
||
target: esp32s3
|
||
sdkconfig: sdkconfig.s3
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
- name: Build
|
||
if: hashFiles('CMakeLists.txt') != ''
|
||
run: |
|
||
git config --global --add safe.directory '*'
|
||
idf.py -B build/${{ matrix.board }} set-target ${{ matrix.target }}
|
||
idf.py -B build/${{ matrix.board }} \
|
||
-D SDKCONFIG_DEFAULTS="sdkconfig.defaults;${{ matrix.sdkconfig }}" \
|
||
-D AUTOFILM_BOARD=${{ matrix.board }} \
|
||
build
|
||
idf.py -B build/${{ matrix.board }} size | tee size-${{ matrix.board }}.txt
|
||
python $IDF_PATH/components/esptool_py/esptool/esptool.py \
|
||
--chip ${{ matrix.target }} merge_bin \
|
||
-o autofilm-${{ matrix.board }}-${{ gitea.sha }}.bin \
|
||
@build/${{ matrix.board }}/flash_args
|
||
- uses: actions/upload-artifact@v3
|
||
if: hashFiles('CMakeLists.txt') != ''
|
||
with:
|
||
name: firmware-${{ matrix.board }}
|
||
path: |
|
||
autofilm-${{ matrix.board }}-*.bin
|
||
build/${{ matrix.board }}/*.bin
|
||
build/${{ matrix.board }}/bootloader/bootloader.bin
|
||
build/${{ matrix.board }}/partition_table/partition-table.bin
|
||
build/${{ matrix.board }}/flasher_args.json
|
||
build/${{ matrix.board }}/sdkconfig
|
||
size-${{ matrix.board }}.txt
|
||
```
|
||
|
||
Context substitutions: Gitea accepts both `github.*` and `gitea.*` in many versions; prefer `gitea.sha` / `gitea.ref` and fall back if the runner is older.
|
||
|
||
`hashFiles` guards keep docs-only stages of this branch from going red before the IDF tree exists.
|
||
|
||
## Required status on PRs
|
||
|
||
Once the IDF skeleton compiles, mark on the `develop` branch protection (Gitea repo settings):
|
||
|
||
- `test`
|
||
- `firmware (wroom)`
|
||
- `firmware (jc4827w543)`
|
||
|
||
No merge to `develop` with a red firmware matrix.
|
||
|
||
## Caching (optional, second pass)
|
||
|
||
- Cache `~/.cache/pip` and IDF tools if not using the pre-baked `espressif/idf` image.
|
||
- Cache `managed_components/` keyed on `idf_component.yml` + `idf.lock`.
|
||
- Do not cache `build/` across boards or targets.
|
||
|
||
## Secrets
|
||
|
||
CI must not need device tokens or Wi-Fi credentials. Firmware builds are offline-configurable. If a later job publishes to a hardware farm, use Gitea secrets — never commit them.
|
||
|
||
## Local equivalent
|
||
|
||
```bash
|
||
cmake -S tests/host -B build/host && cmake --build build/host && ctest --test-dir build/host --output-on-failure
|
||
|
||
docker run --rm -v "$PWD":/project -w /project espressif/idf:v5.4 \
|
||
bash -lc 'idf.py -B build/wroom set-target esp32 && idf.py -B build/wroom -D AUTOFILM_BOARD=wroom build'
|
||
```
|
||
|
||
Agents must run the host tests before pushing once `tests/host` exists.
|