From 464ff1ae67b5ea14de0424dfc36b7520db5c365c Mon Sep 17 00:00:00 2001 From: Hoggormino <125792648+Hoggormino@users.noreply.github.com> Date: Tue, 15 Sep 2026 05:34:44 +0200 Subject: [PATCH 1/2] Add CI build workflow and root Makefile - .github/workflows/build.yml: build the Flipper FAP (ufbt, release + dev SDK channels), the Cloudflare Worker (tsc + wrangler dry-run bundle) and the ESP32-S2 firmware (ESP-IDF v5.2.8) on every push to main and every pull request; syntax-check the inline scripts of the web image-prep page; run `ufbt lint` as a non-blocking advisory job. No path filters, because the shared UART protocol header is compiled into both the FAP and the ESP firmware. - Makefile: one entry point for developers (setup, build-all, build-fap, build-worker, build-esp, launch, flash-esp, serve-web, lint, clean). Works with the stock GNU Make 3.81 on macOS. Auto-detects the ESP-IDF Python virtualenv when export.sh would otherwise look for one matching a newer system python3. - esp32-wifi-fw/sdkconfig.defaults: pin CONFIG_IDF_TARGET="esp32s2" so clean builds (CI, fresh clones) no longer fall back to plain esp32. - cloud-plugins/package.json: add the `build` script (wrangler dry-run bundle) that the Makefile and CI rely on. - README/CONTRIBUTING: document the make targets and CI; add a build badge. - .gitignore: ignore .venv/ and the .clang-format generated by ufbt lint. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/build.yml | 289 +++++++++++++++++++++++++++++++ .gitignore | 5 + CONTRIBUTING.md | 2 +- Makefile | 161 +++++++++++++++++ README.md | 28 +++ cloud-plugins/package.json | 1 + esp32-wifi-fw/sdkconfig.defaults | 4 + 7 files changed, 489 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/build.yml create mode 100644 Makefile diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..b4b50b7 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,289 @@ +# TagTinker CI - builds every deliverable in this monorepo on each push/PR. +# +# Jobs (all independent, all run in parallel on ubuntu-latest): +# fap - Flipper Zero application (.fap) via ufbt, release + dev SDK channels +# worker - Cloudflare Worker (cloud-plugins/) typecheck + wrangler dry-run bundle +# esp32 - ESP32-S2 WiFi devboard firmware via the ESP-IDF v5.2.8 docker image +# web - syntax check of the inline JavaScript in web-image-prep/index.html +# lint - `ufbt lint` (clang-format), ADVISORY ONLY - never fails the run +# +# There are deliberately NO `paths:` filters: esp32-wifi-fw/shared/tt_wifi_proto.h +# is compiled into BOTH the FAP (via shared/tt_wifi_proto_fap.h) and the ESP +# firmware, so a change on either side must always rebuild both. +# +# Deployment of web-image-prep/ to GitHub Pages lives in pages.yml, not here. + +name: Build + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +# One run per ref. A newer push to a PR branch cancels the still-running older +# one; pushes to main are never cancelled so every main commit keeps a result. +concurrency: + group: build-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + # --------------------------------------------------------------------------- + # Flipper Zero application (application.fam at repo root) + # --------------------------------------------------------------------------- + fap: + name: Flipper app (${{ matrix.channel }} SDK) + runs-on: ubuntu-latest + timeout-minutes: 30 + # The `release` channel is the one users run (matches the local ufbt setup: + # firmware 1.4.3 / API 87.1 / f7) and is a hard gate. The `dev` channel + # tracks unreleased firmware and gives early warning of upcoming API + # breaks, so it is allowed to fail without failing the workflow. + continue-on-error: ${{ matrix.experimental }} + strategy: + fail-fast: false + matrix: + include: + - channel: release + experimental: false + - channel: dev + experimental: true + steps: + - name: Checkout + uses: actions/checkout@v7 + + # Installs Python + ufbt, downloads/caches the SDK + toolchain, then runs + # `ufbt -s` inside app-dir. Output lands in /dist. + - name: Build FAP with ufbt + id: build-app + uses: flipperdevices/flipperzero-ufbt-action@v0.1 + with: + app-dir: . + task: build + sdk-channel: ${{ matrix.channel }} + sdk-hw-target: f7 + + - name: Report SDK / toolchain used + env: + CHANNEL: ${{ matrix.channel }} + UFBT_STATUS: ${{ steps.build-app.outputs.ufbt-status }} + API_VERSION: ${{ steps.build-app.outputs.api-version }} + SUFFIX: ${{ steps.build-app.outputs.suffix }} + TOOLCHAIN: ${{ steps.build-app.outputs.toolchain-version }} + FAP_DIR: ${{ steps.build-app.outputs.fap-dir }} + run: | + sdk="$(printf '%s' "$UFBT_STATUS" | jq -r '"\(.version) (hw target \(.target))"' 2>/dev/null || echo unknown)" + { + echo "### Flipper app - ${CHANNEL} channel" + echo "- SDK: ${sdk}" + echo "- API version: ${API_VERSION}" + echo "- Toolchain: ${TOOLCHAIN}" + echo "- Artifact suffix: ${SUFFIX}" + } | tee -a "$GITHUB_STEP_SUMMARY" + ls -l "$FAP_DIR" "$FAP_DIR/debug" + + - name: Upload FAP + debug ELF + uses: actions/upload-artifact@v7 + with: + name: tagtinker-fap-${{ matrix.channel }} + path: | + dist/*.fap + dist/debug/*.elf + if-no-files-found: error + retention-days: 14 + + # --------------------------------------------------------------------------- + # Cloudflare Worker (cloud-plugins/) + # --------------------------------------------------------------------------- + worker: + name: Cloudflare Worker + runs-on: ubuntu-latest + timeout-minutes: 15 + defaults: + run: + working-directory: cloud-plugins + steps: + - name: Checkout + uses: actions/checkout@v7 + + # Node 22 LTS; package.json declares no `engines`, so pin it here. + - name: Set up Node.js + uses: actions/setup-node@v7 + with: + node-version: 22 + cache: npm + cache-dependency-path: cloud-plugins/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Typecheck + run: npx tsc --noEmit + + # `npm run build` = `wrangler deploy --dry-run --outdir dist`: bundles the + # worker without touching Cloudflare, so no account/API token is needed. + - name: Bundle (wrangler dry-run) + run: npm run build + env: + CI: "true" + WRANGLER_SEND_METRICS: "false" + + - name: Upload worker bundle + uses: actions/upload-artifact@v7 + with: + name: tagtinker-worker-bundle + path: cloud-plugins/dist/index.js + if-no-files-found: error + retention-days: 14 + + # --------------------------------------------------------------------------- + # ESP32-S2 WiFi devboard firmware (esp32-wifi-fw/) + # --------------------------------------------------------------------------- + esp32: + name: ESP32-S2 WiFi firmware + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - name: Checkout + uses: actions/checkout@v7 + + # Runs `idf.py build` inside espressif/idf:v5.2.8 (same minor as the + # local ~/esp/esp-idf v5.2.8 install). The action only exports + # IDF_TARGET=esp32s2; sdkconfig.defaults additionally pins + # CONFIG_IDF_TARGET="esp32s2" and sdkconfig itself is gitignored, so a + # clean checkout resolves to the S2 target either way. If the two ever + # disagree idf.py refuses to build, which is what we want. + - name: Build with ESP-IDF v5.2.8 + uses: espressif/esp-idf-ci-action@v1 + with: + esp_idf_version: v5.2.8 + target: esp32s2 + path: esp32-wifi-fw + command: idf.py build + + - name: Verify resolved target and list binaries + working-directory: esp32-wifi-fw + run: | + if ! grep -q '^CONFIG_IDF_TARGET="esp32s2"' sdkconfig; then + echo "::error file=esp32-wifi-fw/sdkconfig.defaults::sdkconfig did not resolve to esp32s2:" + grep '^CONFIG_IDF_TARGET' sdkconfig || echo "(CONFIG_IDF_TARGET not set at all)" + exit 1 + fi + echo "Target OK: $(grep '^CONFIG_IDF_TARGET=' sdkconfig)" + ls -l \ + build/tagtinker_wifi.bin \ + build/bootloader/bootloader.bin \ + build/partition_table/partition-table.bin \ + build/ota_data_initial.bin + + # Paths are kept relative to build/ so the layout inside the artifact + # matches the offsets/paths recorded in flasher_args.json. + - name: Upload firmware images + uses: actions/upload-artifact@v7 + with: + name: tagtinker-esp32s2-firmware + path: | + esp32-wifi-fw/build/tagtinker_wifi.bin + esp32-wifi-fw/build/bootloader/bootloader.bin + esp32-wifi-fw/build/partition_table/partition-table.bin + esp32-wifi-fw/build/ota_data_initial.bin + esp32-wifi-fw/build/flasher_args.json + if-no-files-found: error + retention-days: 14 + + # --------------------------------------------------------------------------- + # Web image-prep tool (web-image-prep/index.html, single self-contained file) + # --------------------------------------------------------------------------- + web: + name: Web image-prep page + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Set up Node.js + uses: actions/setup-node@v7 + with: + node-version: 22 + + # The page has no build step, so the cheapest meaningful gate is to make + # sure every inline