From f71b884369529fe1e32ff243aed81ebe6e05d85b Mon Sep 17 00:00:00 2001 From: "Adrian.Nguyen-Qualgo" Date: Fri, 14 Aug 2026 21:17:23 +0700 Subject: [PATCH 1/4] ci: build matrix, config variants, and failover_connect REQUIRES fix - examples job: matrix of all 5 examples on ESP-IDF v6.0.1 (the version inside espressif32@7.0.1, which is what zen-clock actually pins), plus an early-warning basic_connect build against release-v6.0 (continue-on-error). - config-variants job: covers the three config combinations the 5 examples' own sdkconfig.defaults don't already exercise -- zerocopy (CONFIG_ML_ZERO_COPY_WG=y), minimal (everything optional off), and cellular-zerocopy (both features together, since neither alone would catch an interaction bug between them). Overlays live in ci/, applied via -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.ci" (which replaces the implicit default list, so the base file has to be re-listed alongside the overlay). - examples/failover_connect/main/CMakeLists.txt was missing REQUIRES entirely -- harmless today only because IDF links everything when a component doesn't declare REQUIRES, but it meant this was the one example that would silently build even if `microlink` fell out of the build graph. Set it to match the other 4 examples. espressif/esp-idf-ci-action interpolates `command` inside a single-quoted `bash -c '...'` -- only double quotes are usable inside the command string, noted inline where it matters. --- .github/workflows/ci.yml | 68 +++++++++++++++++++ ci/sdkconfig.ci.cellular-zerocopy | 6 ++ ci/sdkconfig.ci.minimal | 8 +++ ci/sdkconfig.ci.zerocopy | 4 ++ examples/failover_connect/main/CMakeLists.txt | 1 + 5 files changed, 87 insertions(+) create mode 100644 ci/sdkconfig.ci.cellular-zerocopy create mode 100644 ci/sdkconfig.ci.minimal create mode 100644 ci/sdkconfig.ci.zerocopy diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0e625ca..b07436d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,3 +35,71 @@ jobs: - name: Pack component (same validation the registry runs on upload) run: compote -W component pack --project-dir components/microlink --name microlink + + examples: + name: "build ${{ matrix.example }} (${{ matrix.idf_version }})" + runs-on: ubuntu-latest + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + example: + - basic_connect + - cellular_connect + - cellular_heartbeat + - failover_connect + - rebind_test + idf_version: [ v6.0.1 ] + include: + # Early warning for the next ESP-IDF minor release -- allowed to + # fail without turning the whole workflow red. + - example: basic_connect + idf_version: release-v6.0 + continue_on_error: true + steps: + - uses: actions/checkout@v7 + with: + submodules: recursive + + - uses: espressif/esp-idf-ci-action@v1 + continue-on-error: ${{ matrix.continue_on_error == true }} + with: + esp_idf_version: ${{ matrix.idf_version }} + target: esp32s3 + path: "examples/${{ matrix.example }}" + command: "idf.py build" + + config-variants: + name: "build ${{ matrix.variant }}" + runs-on: ubuntu-latest + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + include: + # Each variant overlays ci/sdkconfig.ci. on top of an + # existing example's own sdkconfig.defaults. SDKCONFIG_DEFAULTS + # replaces the implicit default entirely, so the base file has to + # be listed again alongside the overlay. + - variant: zerocopy + base_example: basic_connect + - variant: minimal + base_example: basic_connect + - variant: cellular-zerocopy + base_example: cellular_connect + steps: + - uses: actions/checkout@v7 + with: + submodules: recursive + + - name: Copy overlay into example dir + run: cp "ci/sdkconfig.ci.${{ matrix.variant }}" "examples/${{ matrix.base_example }}/sdkconfig.ci" + + - uses: espressif/esp-idf-ci-action@v1 + with: + esp_idf_version: v6.0.1 + target: esp32s3 + path: "examples/${{ matrix.base_example }}" + # esp-idf-ci-action interpolates `command` inside a single-quoted + # bash -c '...' -- only double quotes are safe to use here. + command: "idf.py -DSDKCONFIG_DEFAULTS=\"sdkconfig.defaults;sdkconfig.ci\" build" diff --git a/ci/sdkconfig.ci.cellular-zerocopy b/ci/sdkconfig.ci.cellular-zerocopy new file mode 100644 index 0000000..9a83680 --- /dev/null +++ b/ci/sdkconfig.ci.cellular-zerocopy @@ -0,0 +1,6 @@ +# Config-variant overlay for CI: zero-copy WireGuard receive combined with +# cellular, on top of cellular_connect's own sdkconfig.defaults (which +# already has CONFIG_ML_ENABLE_CELLULAR=y). Catches interaction bugs between +# the two features that neither one alone would surface. +# See .github/workflows/ci.yml's config-variants job. +CONFIG_ML_ZERO_COPY_WG=y diff --git a/ci/sdkconfig.ci.minimal b/ci/sdkconfig.ci.minimal new file mode 100644 index 0000000..064d49b --- /dev/null +++ b/ci/sdkconfig.ci.minimal @@ -0,0 +1,8 @@ +# Config-variant overlay for CI: everything optional switched off, on top of +# basic_connect's own sdkconfig.defaults -- catches REQUIRES/ifdef mistakes +# that only show up when optional features are compiled OUT, not in. +# See .github/workflows/ci.yml's config-variants job. +CONFIG_ML_ENABLE_CONFIG_HTTPD=n +CONFIG_ML_ZERO_COPY_WG=n +CONFIG_ML_ENABLE_CELLULAR=n +CONFIG_ML_ENABLE_NET_SWITCH=n diff --git a/ci/sdkconfig.ci.zerocopy b/ci/sdkconfig.ci.zerocopy new file mode 100644 index 0000000..6ec08d0 --- /dev/null +++ b/ci/sdkconfig.ci.zerocopy @@ -0,0 +1,4 @@ +# Config-variant overlay for CI: exercises the zero-copy WireGuard receive +# path (contributed by dj-oyu). Applied on top of basic_connect's own +# sdkconfig.defaults -- see .github/workflows/ci.yml's config-variants job. +CONFIG_ML_ZERO_COPY_WG=y diff --git a/examples/failover_connect/main/CMakeLists.txt b/examples/failover_connect/main/CMakeLists.txt index 00ee335..98dd7ad 100644 --- a/examples/failover_connect/main/CMakeLists.txt +++ b/examples/failover_connect/main/CMakeLists.txt @@ -1,4 +1,5 @@ idf_component_register( SRCS "main.c" INCLUDE_DIRS "." + REQUIRES microlink nvs_flash esp_netif esp_event driver ) From b9c7b7a4bcbacad9519301642b24a9d93782c602 Mon Sep 17 00:00:00 2001 From: "Adrian.Nguyen-Qualgo" Date: Fri, 14 Aug 2026 21:17:23 +0700 Subject: [PATCH 2/4] ci: add upstream drift watcher for CamM2325/microlink Same pattern as fugo101/wireguard-lwip's watcher: weekly cron + workflow_dispatch, a single tracking issue updated in place and auto-closed when clean. git cherry -v against upstream/main catches missing commits by patch-id; gh pr list catches open PRs, filtered against the PR numbers UPSTREAM_PRS.md already accounts for (absorbed or deliberately skipped with a recorded reason) so the issue only surfaces genuinely new drift. Requires the upstream-drift label (created on the repo, not tracked in git). --- .github/workflows/upstream-drift.yml | 86 ++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 .github/workflows/upstream-drift.yml diff --git a/.github/workflows/upstream-drift.yml b/.github/workflows/upstream-drift.yml new file mode 100644 index 0000000..c5c8e2e --- /dev/null +++ b/.github/workflows/upstream-drift.yml @@ -0,0 +1,86 @@ +name: Upstream drift + +on: + schedule: + - cron: '17 6 * * 1' # Mondays 06:17 UTC + workflow_dispatch: + +permissions: + contents: read + issues: write + +concurrency: + group: upstream-drift + cancel-in-progress: false + +jobs: + check: + runs-on: ubuntu-latest + timeout-minutes: 10 + env: + GH_TOKEN: ${{ github.token }} + UPSTREAM: CamM2325/microlink + TITLE: 'Upstream drift: CamM2325/microlink' + # PR numbers already accounted for in UPSTREAM_PRS.md (absorbed or + # deliberately skipped with a recorded reason) -- excluded so the + # tracking issue only ever shows genuinely new drift. + KNOWN_PRS: '20 21 22 23 24 25 14 3' + steps: + - uses: actions/checkout@v7 + with: { fetch-depth: 0 } + + - name: Collect drift + id: drift + run: | + set -euo pipefail + git fetch --no-tags --quiet origin main # this repo IS the fork; origin's parent is upstream + # This repo is a real GitHub fork of $UPSTREAM. We don't have a local + # remote pointed at it (no need to -- GitHub tracks the fork relationship + # directly), so fetch it by URL for the comparison. + git fetch --no-tags --quiet "https://github.com/$UPSTREAM.git" main:refs/remotes/upstream-check/main + + # `git cherry ` lists commits in missing from + # by patch-id, prefixing '-' when already present. Reversing + # the arguments gives upstream commits we lack. + COMMITS="$(git cherry -v HEAD upstream-check/main | sed -n 's/^+ //p' || true)" + + PRS="" + while read -r n title; do + [ -n "$n" ] || continue + case " $KNOWN_PRS " in *" $n "*) continue ;; esac + PRS="${PRS}- [#${n}](https://github.com/${UPSTREAM}/pull/${n}) — ${title}"$'\n' + done < <(gh pr list --repo "$UPSTREAM" --state open --limit 100 \ + --json number,title --jq '.[] | "\(.number) \(.title)"') + + { + echo 'body<> "$GITHUB_OUTPUT" + + if [ -n "$COMMITS$PRS" ]; then echo 'drifted=true' >> "$GITHUB_OUTPUT"; + else echo 'drifted=false' >> "$GITHUB_OUTPUT"; fi + + - name: Open / update / close the tracking issue + run: | + set -euo pipefail + NUM="$(gh issue list --state open --label upstream-drift --limit 1 --json number --jq '.[0].number // empty')" + if [ "${{ steps.drift.outputs.drifted }}" = 'true' ]; then + if [ -n "$NUM" ]; then + gh issue edit "$NUM" --body "${{ steps.drift.outputs.body }}" + else + gh issue create --title "$TITLE" --label upstream-drift \ + --body "${{ steps.drift.outputs.body }}" + fi + elif [ -n "$NUM" ]; then + gh issue close "$NUM" --comment 'No upstream drift remaining.' + fi From d422537fc7ca78a8ddfc2145c904bd864ef0649c Mon Sep 17 00:00:00 2001 From: "Adrian.Nguyen-Qualgo" Date: Fri, 14 Aug 2026 21:26:05 +0700 Subject: [PATCH 3/4] fix: REQUIRES cannot be conditioned on Kconfig -- make it unconditional Component-requirements expansion (which decides REQUIRES/PRIV_REQUIRES, and therefore public include-dir propagation) runs in an ESP-IDF CMake pass that happens before Kconfig is resolved -- REQUIRES can never depend on a CONFIG_* value, only SRCS can (in the later registration pass). The previous commit (ea9237a) gated the esp_driver_uart/ esp_driver_gpio REQUIRES on CONFIG_ML_ENABLE_CELLULAR, which silently no-ops: confirmed the fix never actually took effect, in both a local PlatformIO build and the real ESP-IDF Docker CI added by this same PR (everything with CONFIG_ML_ENABLE_CELLULAR=y still failed with "driver/ uart.h: No such file or directory"). Fix: make both REQUIRES unconditional, same as `driver` and esp_driver_tsens already are above them -- REQUIRES is necessarily coarser-grained than SRCS in ESP-IDF, this is normal. --- components/microlink/CMakeLists.txt | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/components/microlink/CMakeLists.txt b/components/microlink/CMakeLists.txt index b856c39..72f1b4e 100644 --- a/components/microlink/CMakeLists.txt +++ b/components/microlink/CMakeLists.txt @@ -36,17 +36,19 @@ set(REQUIRES driver esp_driver_tsens esp_http_server -) - -if(CONFIG_ML_ENABLE_CELLULAR) # ml_cellular.c / ml_at_socket.c include driver/uart.h and driver/gpio.h. On # ESP-IDF 6.x those headers live in esp_driver_uart / esp_driver_gpio, not in # the umbrella `driver` component above -- `driver` only PRIV_REQUIRES # esp_driver_gpio (private, doesn't propagate) and doesn't require - # esp_driver_uart at all. Without these, any CONFIG_ML_ENABLE_CELLULAR=y - # build fails to find both headers. - list(APPEND REQUIRES esp_driver_uart esp_driver_gpio) -endif() + # esp_driver_uart at all. Unconditional, not gated on CONFIG_ML_ENABLE_CELLULAR: + # REQUIRES/PRIV_REQUIRES are resolved during ESP-IDF's component-requirements + # expansion pass, which runs before Kconfig is available -- REQUIRES can never + # be conditioned on a CONFIG_* value (only SRCS can, in the later registration + # pass). Confirmed empirically: a Kconfig-gated REQUIRES here silently no-ops + # in every environment tried (PlatformIO and real ESP-IDF Docker CI alike). + esp_driver_uart + esp_driver_gpio +) idf_component_register( SRCS ${SRCS} From 6d416a8fc87721c69357e88bb044737dafbe0cc4 Mon Sep 17 00:00:00 2001 From: "Adrian.Nguyen-Qualgo" Date: Fri, 14 Aug 2026 21:26:05 +0700 Subject: [PATCH 4/4] fix: avoid tautological comparison in old-format peer migration config_load_peers()'s old-format migration path compared old_count (uint8_t, max 255) directly against ML_CONFIG_MAX_ALLOWED_PEERS. Every example in this repo defaults that Kconfig value to 512 -- since 255 is always <= 512, GCC's -Werror=type-limits correctly flags the comparison as always true regardless of old_count's runtime value, which is a real bug: on a config with a *smaller* max (the Kconfig range allows as low as 1), the intended clamp would silently never trigger either, since the compiler would have already proven the check meaningless for the default and this path is only reachable at all when it isn't. This was pre-existing on main, unrelated to this migration -- it just never had CI to catch it before. Fix: widen old_count to uint16_t before the comparison, so the check is against the variable's actual runtime value instead of a type range GCC can resolve at compile time. --- components/microlink/src/ml_config_httpd.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/components/microlink/src/ml_config_httpd.c b/components/microlink/src/ml_config_httpd.c index 8033dc0..392042c 100644 --- a/components/microlink/src/ml_config_httpd.c +++ b/components/microlink/src/ml_config_httpd.c @@ -163,7 +163,11 @@ static void config_load_peers(ml_config_ctx_t *ctx) { size_t rlen = stored_len; err = nvs_get_blob(ctx->nvs, NVS_KEY_PEERS, tmp, &rlen); if (err == ESP_OK) { - uint8_t old_count = tmp[0]; + /* old_count is widened to uint16_t before the comparison: comparing the + * uint8_t directly against ML_CONFIG_MAX_ALLOWED_PEERS is tautological + * whenever that Kconfig value is >= 255 (true for the default of 512), + * which -Werror=type-limits correctly flags as always-true. */ + uint16_t old_count = tmp[0]; uint16_t count = (old_count <= ML_CONFIG_MAX_ALLOWED_PEERS) ? old_count : 0; memset(&ctx->peer_list, 0, sizeof(ctx->peer_list)); ctx->peer_list.count = count;