Add async firmware cache clearing for multi-bond support #123
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build & Release | |
| on: | |
| push: | |
| branches: [ main ] | |
| pull_request: | |
| branches: [ main ] | |
| workflow_dispatch: | |
| inputs: | |
| bump_version: | |
| description: 'Create Release' | |
| required: false | |
| default: 'none' | |
| type: choice | |
| options: | |
| - none | |
| - patch | |
| - minor | |
| - major | |
| jobs: | |
| # Bump version if requested (only for workflow_dispatch) | |
| bump-version: | |
| if: github.event_name == 'workflow_dispatch' && github.event.inputs.bump_version != 'none' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| outputs: | |
| version_bumped: ${{ steps.bump.outputs.bumped }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Bump version | |
| id: bump | |
| run: | | |
| CURRENT_VERSION=$(cat VERSION | tr -d '[:space:]') | |
| echo "Current version: $CURRENT_VERSION" | |
| # Parse version (assuming semver X.Y.Z) | |
| IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION" | |
| case "${{ github.event.inputs.bump_version }}" in | |
| patch) | |
| PATCH=$((PATCH + 1)) | |
| ;; | |
| minor) | |
| MINOR=$((MINOR + 1)) | |
| PATCH=0 | |
| ;; | |
| major) | |
| MAJOR=$((MAJOR + 1)) | |
| MINOR=0 | |
| PATCH=0 | |
| ;; | |
| esac | |
| NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}" | |
| echo "New version: $NEW_VERSION" | |
| echo "$NEW_VERSION" > VERSION | |
| echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT | |
| echo "bumped=true" >> $GITHUB_OUTPUT | |
| - name: Commit and push version bump | |
| uses: stefanzweifel/git-auto-commit-action@v5 | |
| with: | |
| commit_message: "Bump version to ${{ steps.bump.outputs.new_version }}" | |
| file_pattern: VERSION | |
| commit_user_name: ${{ github.actor }} | |
| commit_user_email: ${{ github.actor }}@users.noreply.github.com | |
| commit_author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com> | |
| # Check if VERSION has a release tag (determines if we should build/release) | |
| check-version-tag: | |
| needs: [bump-version] | |
| if: always() | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.version.outputs.version }} | |
| has_release: ${{ steps.check.outputs.has_release }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| ref: main # Always fetch latest main to get version bump commit | |
| - name: Read VERSION file | |
| id: version | |
| run: | | |
| VERSION=$(cat VERSION | tr -d '[:space:]') | |
| echo "version=${VERSION}" >> $GITHUB_OUTPUT | |
| echo "π¦ Current VERSION: ${VERSION}" | |
| - name: Check if version tag exists | |
| id: check | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| # Check if tag exists (locally or remotely) | |
| if git rev-parse "v${VERSION}" >/dev/null 2>&1; then | |
| echo "has_release=true" >> $GITHUB_OUTPUT | |
| echo "βοΈ Version v${VERSION} already has a release" | |
| else | |
| echo "has_release=false" >> $GITHUB_OUTPUT | |
| echo "β Version v${VERSION} does not have a release - will build & release" | |
| fi | |
| # Always run security scan | |
| check-security: | |
| needs: [bump-version] | |
| if: always() | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Run security scan | |
| run: | | |
| echo "Running security analysis..." | |
| # Check for common security issues in C code | |
| find ncs/app/src -name '*.c' -o -name '*.h' | xargs grep -l 'strcpy\|sprintf\|gets' || echo 'No obvious security issues found' | |
| # Check which directories changed | |
| check-changes: | |
| needs: [bump-version] | |
| if: always() | |
| runs-on: ubuntu-latest | |
| outputs: | |
| ncs_changed: ${{ steps.changes.outputs.ncs }} | |
| esp_changed: ${{ steps.changes.outputs.esp }} | |
| version_changed: ${{ steps.changes.outputs.version }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| ref: main # Always fetch latest main to get version bump commit | |
| - name: Check for file changes | |
| id: changes | |
| run: | | |
| # If version was bumped by workflow, mark version as changed | |
| if [ "${{ needs.bump-version.outputs.version_bumped }}" = "true" ]; then | |
| echo "version=true" >> $GITHUB_OUTPUT | |
| echo "β VERSION bumped by workflow - will build all firmware" | |
| else | |
| # For PRs, compare against base branch | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| BASE_SHA="${{ github.event.pull_request.base.sha }}" | |
| HEAD_SHA="${{ github.event.pull_request.head.sha }}" | |
| else | |
| # For pushes, compare with previous commit | |
| BASE_SHA="${{ github.event.before }}" | |
| HEAD_SHA="${{ github.sha }}" | |
| fi | |
| echo "Comparing $BASE_SHA...$HEAD_SHA" | |
| # Check if VERSION file changed | |
| if git diff --name-only $BASE_SHA $HEAD_SHA | grep -q '^VERSION$'; then | |
| echo "version=true" >> $GITHUB_OUTPUT | |
| echo "β VERSION file changed - will build all firmware" | |
| else | |
| echo "version=false" >> $GITHUB_OUTPUT | |
| echo "βοΈ VERSION file unchanged" | |
| fi | |
| fi | |
| # Check if ncs directory changed | |
| if git diff --name-only $BASE_SHA $HEAD_SHA | grep -q '^ncs/'; then | |
| echo "ncs=true" >> $GITHUB_OUTPUT | |
| echo "β ncs/ directory has changes" | |
| else | |
| echo "ncs=false" >> $GITHUB_OUTPUT | |
| echo "βοΈ ncs/ directory unchanged" | |
| fi | |
| # Check if esp directory changed | |
| if git diff --name-only $BASE_SHA $HEAD_SHA | grep -q '^esp/'; then | |
| echo "esp=true" >> $GITHUB_OUTPUT | |
| echo "β esp/ directory has changes" | |
| else | |
| echo "esp=false" >> $GITHUB_OUTPUT | |
| echo "βοΈ esp/ directory unchanged" | |
| fi | |
| # Build nRF52 firmware | |
| build-nrf52: | |
| needs: [check-security, check-changes, check-version-tag] | |
| # Build if: VERSION file changed OR ncs directory changed OR VERSION doesn't have a release | |
| if: always() && (needs.check-changes.outputs.version_changed == 'true' || needs.check-changes.outputs.ncs_changed == 'true' || needs.check-version-tag.outputs.has_release == 'false') | |
| runs-on: ubuntu-latest | |
| container: | |
| image: ghcr.io/zephyrproject-rtos/ci:v0.26.6 | |
| env: | |
| CMAKE_PREFIX_PATH: /opt/toolchains | |
| strategy: | |
| matrix: | |
| board: [seeed_xiao_nrf52840, adafruit_feather_nrf52840, nordic_nrf52840dongle, aprbrother_nrf52840, raytac_mdbt50q_rx, raytac_mdbt50q_cx_40, makerdiary_nrf52840mdk] | |
| include: | |
| - board: seeed_xiao_nrf52840 | |
| description: "Seeed XIAO nRF52840" | |
| board_target: xiao_ble | |
| overlay: seeed_xiao_nrf52840.overlay | |
| conf: seeed_xiao_nrf52840.conf | |
| - board: adafruit_feather_nrf52840 | |
| description: "Adafruit Feather nRF52840 Express" | |
| board_target: adafruit_feather_nrf52840 | |
| - board: nordic_nrf52840dongle | |
| description: "Nordic nRF52840 Dongle (PCA10059)" | |
| board_target: nrf52840dongle | |
| overlay: nordic_nrf52840dongle.overlay | |
| conf: nordic_nrf52840dongle.conf | |
| - board: aprbrother_nrf52840 | |
| description: "April Brother nRF52840 Dongle" | |
| board_target: nrf52840dongle | |
| overlay: aprbrother_nrf52840.overlay | |
| conf: aprbrother_nrf52840.conf | |
| - board: raytac_mdbt50q_rx | |
| description: "Raytac MDBT50Q-RX Dongle" | |
| board_target: nrf52840dongle | |
| overlay: raytac_mdbt50q_rx.overlay | |
| conf: raytac_mdbt50q_rx.conf | |
| - board: raytac_mdbt50q_cx_40 | |
| description: "Raytac MDBT50Q-CX-40 (Nordic DFU)" | |
| board_target: raytac_mdbt50q_cx_40/nrf52840 | |
| overlay: raytac_mdbt50q_cx_40.overlay | |
| conf: raytac_mdbt50q_cx_40.conf | |
| uses_dfu: true | |
| - board: makerdiary_nrf52840mdk | |
| description: "MakerDiary nRF52840 MDK USB Dongle" | |
| board_target: nrf52840dongle | |
| overlay: makerdiary_nrf52840mdk.overlay | |
| conf: makerdiary_nrf52840mdk.conf | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| submodules: recursive | |
| ref: main # Always fetch latest main to get version bump commit | |
| - name: πΎ Cache Zephyr Workspace | |
| id: cache-workspace | |
| uses: actions/cache@v4 | |
| with: | |
| path: /zephyr_workspace | |
| key: zephyr-workspace-ncs-v3.1.0-${{ runner.os }} | |
| restore-keys: | | |
| zephyr-workspace-ncs-v3.1.0- | |
| - name: β»οΈ Initialize Zephyr Workspace | |
| if: steps.cache-workspace.outputs.cache-hit != 'true' | |
| run: | | |
| mkdir -p /zephyr_workspace | |
| cd /zephyr_workspace | |
| west init -m https://github.com/nrfconnect/sdk-nrf.git --mr v3.1.0 | |
| west update --narrow -o=--depth=1 | |
| - name: π Copy App and Board Files to Workspace | |
| run: | | |
| rm -rf /zephyr_workspace/app | |
| rm -rf /zephyr_workspace/build | |
| # Create app directory structure matching local layout | |
| mkdir -p /zephyr_workspace/ncs | |
| cp -r ncs/app /zephyr_workspace/ncs/app | |
| cp VERSION /zephyr_workspace/VERSION | |
| # Symlink for compatibility with build commands | |
| ln -s /zephyr_workspace/ncs/app /zephyr_workspace/app | |
| - name: πΎ Cache ccache | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.cache/ccache | |
| key: ccache-v1-${{ runner.os }}-${{ matrix.board }}-${{ github.sha }} | |
| restore-keys: | | |
| ccache-v1-${{ runner.os }}-${{ matrix.board }}- | |
| ccache-v1-${{ runner.os }}- | |
| - name: π¨ Build Project | |
| run: | | |
| cd /zephyr_workspace | |
| ccache -z | |
| # Build with overlay/conf if specified, otherwise use board defaults | |
| if [ -n "${{ matrix.overlay }}" ]; then | |
| echo "Building ${{ matrix.description }} with custom overlay/conf" | |
| west build \ | |
| --board ${{ matrix.board_target }} \ | |
| --pristine=always app \ | |
| -- \ | |
| -DBOARD_ROOT="/zephyr_workspace/ncs/app" \ | |
| -DDTC_OVERLAY_FILE="/zephyr_workspace/ncs/app/boards/${{ matrix.overlay }}" \ | |
| -DEXTRA_CONF_FILE="/zephyr_workspace/ncs/app/boards/${{ matrix.conf }}" | |
| else | |
| echo "Building ${{ matrix.description }} with board defaults" | |
| west build \ | |
| --board ${{ matrix.board_target }} \ | |
| --pristine=always app \ | |
| -- \ | |
| -DBOARD_ROOT="/zephyr_workspace/ncs/app" | |
| fi | |
| ccache -sv | |
| - name: π¦ Prepare Firmware Artifacts | |
| run: | | |
| cd /zephyr_workspace | |
| # Always use version from check-version-tag | |
| FILENAME_VERSION="${{ needs.check-version-tag.outputs.version }}" | |
| # Clean up any old firmware files from cached workspace | |
| rm -f mp_usb_*.uf2 mp_usb_*.zip mp_usb_*.hex | |
| # Check if this board uses DFU (Nordic bootloader) | |
| if [ "${{ matrix.uses_dfu }}" = "true" ]; then | |
| echo "π¦ Creating DFU package for ${{ matrix.board }}..." | |
| # Install nrfutil if not present | |
| pip3 install --user nrfutil >/dev/null 2>&1 || true | |
| # Create DFU package for command-line flashing (application-version must be integer) | |
| ~/.local/bin/nrfutil pkg generate --hw-version 52 --sd-req 0x00 \ | |
| --application-version 1 \ | |
| --application build/app/zephyr/zephyr.hex \ | |
| mp_usb_${FILENAME_VERSION}_${{ matrix.board }}.zip | |
| # Also copy HEX file for nRF Connect for Desktop Programmer (GUI) | |
| cp build/app/zephyr/zephyr.hex mp_usb_${FILENAME_VERSION}_${{ matrix.board }}.hex | |
| echo "β ${{ matrix.board }} DFU ZIP: mp_usb_${FILENAME_VERSION}_${{ matrix.board }}.zip" | |
| echo "β ${{ matrix.board }} HEX: mp_usb_${FILENAME_VERSION}_${{ matrix.board }}.hex" | |
| else | |
| # Copy and rename UF2 files | |
| cp build/app/zephyr/zephyr.uf2 mp_usb_${FILENAME_VERSION}_${{ matrix.board }}.uf2 | |
| echo "β ${{ matrix.board }} UF2: mp_usb_${FILENAME_VERSION}_${{ matrix.board }}.uf2" | |
| fi | |
| ls -la mp_usb_* | |
| - name: π¦ Upload Firmware Artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: mp_usb_${{ matrix.board }} | |
| path: /zephyr_workspace/mp_usb_* | |
| retention-days: 90 | |
| # Build ESP32 firmware | |
| build-esp32: | |
| needs: [check-security, check-changes, check-version-tag] | |
| # Build if: VERSION file changed OR esp directory changed OR VERSION doesn't have a release | |
| if: always() && (needs.check-changes.outputs.version_changed == 'true' || needs.check-changes.outputs.esp_changed == 'true' || needs.check-version-tag.outputs.has_release == 'false') | |
| runs-on: ubuntu-latest | |
| strategy: | |
| matrix: | |
| board: | |
| - name: seeed_xiao_esp32s3 | |
| display_name: "Seeed XIAO ESP32-S3" | |
| sdkconfig: "sdkconfig.defaults;sdkconfig.board.xiao" | |
| - name: lilygo_tdisplay_s3 | |
| display_name: "LilyGo T-Display-S3" | |
| sdkconfig: "sdkconfig.defaults;sdkconfig.board.lilygo" | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: recursive | |
| ref: main # Always fetch latest main to get version bump commit | |
| - name: Build ${{ matrix.board.display_name }} | |
| uses: espressif/esp-idf-ci-action@v1 | |
| with: | |
| esp_idf_version: release-v6.0 | |
| target: esp32s3 | |
| path: esp | |
| command: | | |
| . ~/esp-idf/export.sh | |
| cd esp | |
| export SDKCONFIG_DEFAULTS="${{ matrix.board.sdkconfig }}" | |
| idf.py build | |
| - name: Prepare artifacts | |
| run: | | |
| mkdir -p artifacts | |
| # Always use version from check-version-tag | |
| FILENAME_VERSION="${{ needs.check-version-tag.outputs.version }}" | |
| # Copy app binary only (preserves NVS/BLE bonds when flashed at 0x10000) | |
| cp esp/build/mouthpad_usb.bin artifacts/mp_usb_${FILENAME_VERSION}_${{ matrix.board.name }}.bin | |
| echo "β ${{ matrix.board.display_name }} app binary created" | |
| ls -lh artifacts/ | |
| - name: Upload firmware artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: mp_usb_${{ matrix.board.name }} | |
| path: artifacts/mp_usb_*.bin | |
| if-no-files-found: error | |
| retention-days: 90 | |
| # Create release (only if version tag doesn't exist yet) | |
| create-release: | |
| needs: [build-nrf52, build-esp32, check-version-tag] | |
| if: always() && needs.check-version-tag.outputs.has_release == 'false' && (needs.build-nrf52.result == 'success' || needs.build-nrf52.result == 'skipped') && (needs.build-esp32.result == 'success' || needs.build-esp32.result == 'skipped') | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: artifacts | |
| - name: Prepare release package | |
| run: | | |
| mkdir -p release | |
| VERSION="${{ needs.check-version-tag.outputs.version }}" | |
| echo "π¦ Collecting firmware files for v${VERSION} release:" | |
| # Copy all firmware files from artifacts | |
| find artifacts -type f \( -name "*.uf2" -o -name "*.bin" -o -name "*.zip" -o -name "*.hex" \) -exec cp {} release/ \; | |
| echo "" | |
| echo "β Release package contents:" | |
| ls -lh release/ | |
| # Create checksums | |
| cd release | |
| sha256sum * > checksums.txt | |
| echo "" | |
| echo "π Checksums:" | |
| cat checksums.txt | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@v1 | |
| with: | |
| tag_name: v${{ needs.check-version-tag.outputs.version }} | |
| name: MouthPad USB v${{ needs.check-version-tag.outputs.version }} | |
| body: | | |
| ## MouthPad USB Firmware v${{ needs.check-version-tag.outputs.version }} | |
| ### π¦ Firmware Files | |
| **nRF52840 Boards (UF2 format - drag & drop to bootloader):** | |
| - `mp_usb_${{ needs.check-version-tag.outputs.version }}_seeed_xiao_nrf52840.uf2` - Seeed XIAO nRF52840 | |
| - `mp_usb_${{ needs.check-version-tag.outputs.version }}_adafruit_feather_nrf52840.uf2` - Adafruit Feather nRF52840 Express | |
| - `mp_usb_${{ needs.check-version-tag.outputs.version }}_nordic_nrf52840dongle.uf2` - Nordic nRF52840 Dongle | |
| - `mp_usb_${{ needs.check-version-tag.outputs.version }}_aprbrother_nrf52840.uf2` - April Brother nRF52840 Dongle | |
| - `mp_usb_${{ needs.check-version-tag.outputs.version }}_raytac_mdbt50q_rx.uf2` - Raytac MDBT50Q-RX Dongle | |
| - `mp_usb_${{ needs.check-version-tag.outputs.version }}_makerdiary_nrf52840mdk.uf2` - MakerDiary nRF52840 MDK USB Dongle | |
| **nRF52840 Boards (Nordic DFU bootloader - Raytac CX-40):** | |
| - `mp_usb_${{ needs.check-version-tag.outputs.version }}_raytac_mdbt50q_cx_40.hex` - HEX file (for nRF Connect Programmer GUI) | |
| - `mp_usb_${{ needs.check-version-tag.outputs.version }}_raytac_mdbt50q_cx_40.zip` - DFU package (for command-line nrfutil) | |
| **ESP32-S3 Boards (BIN format - flash with esptool):** | |
| - `mp_usb_${{ needs.check-version-tag.outputs.version }}_seeed_xiao_esp32s3.bin` - Seeed XIAO ESP32-S3 | |
| - `mp_usb_${{ needs.check-version-tag.outputs.version }}_lilygo_tdisplay_s3.bin` - LilyGo T-Display-S3 | |
| ### π§ Installation | |
| **nRF52840 (UF2 - most boards):** | |
| 1. Enter bootloader mode (double-tap reset button) | |
| 2. Drag & drop the `.uf2` file to the mounted drive | |
| 3. Device will automatically reboot with new firmware | |
| **nRF52840 (Nordic DFU for Raytac CX-40):** | |
| *Option 1 - GUI with HEX file (Recommended):* | |
| 1. Download [nRF Connect for Desktop](https://www.nordicsemi.com/Products/Development-tools/nRF-Connect-for-Desktop) | |
| 2. Install the "Programmer" app | |
| 3. Put device in bootloader mode (hold RESET button while connecting USB) | |
| 4. Select device in nRF Connect Programmer | |
| 5. Add the `.hex` file and click "Write" | |
| *Option 2 - Command Line with ZIP package:* | |
| 1. Install nrfutil: `pip install nrfutil` | |
| 2. Put device in bootloader mode (hold RESET button while connecting USB) | |
| 3. Flash: `nrfutil dfu serial -pkg mp_usb_<version>_raytac_mdbt50q_cx_40.zip -p <PORT>` | |
| 4. Device will automatically reboot with new firmware | |
| **ESP32-S3 (BIN):** | |
| ```bash | |
| esptool.py --chip esp32s3 write_flash 0x10000 mp_usb_<version>_<board>.bin | |
| ``` | |
| **Note:** | |
| - UF2 and DFU preserve Settings/NVS storage including BLE bonds | |
| - ESP32-S3: Flashing at 0x10000 (app partition) preserves NVS storage | |
| ### π Checksums | |
| See `checksums.txt` for SHA256 verification. | |
| files: | | |
| release/* | |
| draft: false | |
| prerelease: false | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Release Summary | |
| run: | | |
| VERSION="${{ needs.check-version-tag.outputs.version }}" | |
| echo "### β Release v${VERSION} Created" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Version:** \`v${VERSION}\`" >> $GITHUB_STEP_SUMMARY | |
| echo "**Firmware files:** $(ls release/ | grep -E '\.(uf2|bin|zip|hex)$' | wc -l)" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "π¦ **Release contents:**" >> $GITHUB_STEP_SUMMARY | |
| echo "\`\`\`" >> $GITHUB_STEP_SUMMARY | |
| ls -lh release/ >> $GITHUB_STEP_SUMMARY | |
| echo "\`\`\`" >> $GITHUB_STEP_SUMMARY |