diff options
| author | HiFiPhile <[email protected]> | 2026-06-22 21:30:58 +0200 |
|---|---|---|
| committer | HiFiPhile <[email protected]> | 2026-06-22 21:30:58 +0200 |
| commit | 693cdce08e14833f26f4e8a1f26e4fd546be4c35 (patch) | |
| tree | 7667d2223dc32d9f21b5b60ab200e64ed46e3e20 /.github | |
| parent | 41e9eaa65a935136085d78ec4b99c81ff991b560 (diff) | |
| parent | cd3561bf158afd5a5718904b8139a338d1e3b67c (diff) | |
Merge remote-tracking branch 'tinyusb/master' into pr-osal-spin-deinit
Signed-off-by: HiFiPhile <[email protected]>
Diffstat (limited to '.github')
26 files changed, 1276 insertions, 566 deletions
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..e9930a25a --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: hathach diff --git a/.github/actions/get_deps/action.yml b/.github/actions/get_deps/action.yml index ae9e7bbef..ff6972af6 100644 --- a/.github/actions/get_deps/action.yml +++ b/.github/actions/get_deps/action.yml @@ -9,8 +9,12 @@ runs: using: "composite" steps: - name: Checkout pico-sdk for rp2040 - if: contains(inputs.arg, 'rp2040') || contains(inputs.arg, 'raspberry_pi_pico') - uses: actions/checkout@v4 + if: >- + contains(inputs.arg, 'rp2040') || + contains(inputs.arg, 'rp2350') || + contains(inputs.arg, 'raspberry_pi_pico') || + contains(inputs.arg, 'adafruit_fruit_jam') + uses: actions/checkout@v6 with: repository: raspberrypi/pico-sdk ref: master @@ -19,11 +23,17 @@ runs: - name: Linux dependencies if: runner.os == 'Linux' run: | - sudo apt install -y ninja-build + NINJA_URL=https://github.com/ninja-build/ninja/releases/download/v1.13.1/ninja-linux.zip + wget $NINJA_URL -O ninja-linux.zip + unzip ninja-linux.zip -d ninja-bin + pip install membrowse + echo >> $GITHUB_PATH "${{ github.workspace }}/ninja-bin" shell: bash - name: Get Dependencies + env: + ARG: ${{ inputs.arg }} run: | - python3 tools/get_deps.py ${{ inputs.arg }} + python3 tools/get_deps.py ${ARG} echo "PICO_SDK_PATH=${{ github.workspace }}/pico-sdk" >> $GITHUB_ENV shell: bash diff --git a/.github/actions/setup_toolchain/action.yml b/.github/actions/setup_toolchain/action.yml index 6fd5c9d4e..f78fe4dd3 100644 --- a/.github/actions/setup_toolchain/action.yml +++ b/.github/actions/setup_toolchain/action.yml @@ -13,12 +13,6 @@ outputs: runs: using: "composite" steps: - - name: Install ARM GCC - if: inputs.toolchain == 'arm-gcc' - uses: carlosperate/arm-none-eabi-gcc-action@v1 - with: - release: '14.2.Rel1' - - name: Pull ESP-IDF docker if: inputs.toolchain == 'esp-idf' uses: ./.github/actions/setup_toolchain/espressif @@ -26,20 +20,18 @@ runs: toolchain: ${{ inputs.toolchain }} - name: Get Toolchain URL - if: >- - inputs.toolchain != 'arm-gcc' && - inputs.toolchain != 'esp-idf' + if: inputs.toolchain != 'esp-idf' id: set-toolchain-url + env: + TOOLCHAIN: ${{ inputs.toolchain }} run: | - TOOLCHAIN_URL=$(jq -r '."${{ inputs.toolchain }}"' .github/actions/setup_toolchain/toolchain.json) + TOOLCHAIN_URL=$(jq -r --arg tc "$TOOLCHAIN" '.[$tc]' .github/actions/setup_toolchain/toolchain.json) echo "toolchain_url=$TOOLCHAIN_URL" echo "toolchain_url=$TOOLCHAIN_URL" >> $GITHUB_OUTPUT shell: bash - name: Download Toolchain - if: >- - inputs.toolchain != 'arm-gcc' && - inputs.toolchain != 'esp-idf' + if: inputs.toolchain != 'esp-idf' uses: ./.github/actions/setup_toolchain/download with: toolchain: ${{ inputs.toolchain }} @@ -47,11 +39,13 @@ runs: - name: Set toolchain option id: set-toolchain-option + env: + TOOLCHAIN: ${{ inputs.toolchain }} run: | BUILD_OPTION="" - if [[ "${{ inputs.toolchain }}" == *"clang"* ]]; then + if [[ "$TOOLCHAIN" == *"clang"* ]]; then BUILD_OPTION="--toolchain clang" - elif [[ "${{ inputs.toolchain }}" == "arm-iar" ]]; then + elif [[ "$TOOLCHAIN" == "arm-iar" ]]; then BUILD_OPTION="--toolchain iar" fi echo "build_option=$BUILD_OPTION" diff --git a/.github/actions/setup_toolchain/download/action.yml b/.github/actions/setup_toolchain/download/action.yml index ce9643010..77d3bcf19 100644 --- a/.github/actions/setup_toolchain/download/action.yml +++ b/.github/actions/setup_toolchain/download/action.yml @@ -13,7 +13,7 @@ runs: steps: - name: Cache Toolchain if: ${{ !startsWith(inputs.toolchain_url, 'https://github.com') }} - uses: actions/cache@v4 + uses: actions/cache@v5 id: cache-toolchain-download with: path: ~/cache/${{ inputs.toolchain }} @@ -21,27 +21,58 @@ runs: - name: Install Toolchain if: steps.cache-toolchain-download.outputs.cache-hit != 'true' + env: + TOOLCHAIN: ${{ inputs.toolchain }} + TOOLCHAIN_URL: ${{ inputs.toolchain_url }} run: | - mkdir -p ~/cache/${{ inputs.toolchain }} + mkdir -p ~/cache/${TOOLCHAIN} + FILE_EXT="${TOOLCHAIN_URL##*.}" - if [[ ${{ inputs.toolchain }} == rx-gcc ]]; then - wget --progress=dot:giga ${{ inputs.toolchain_url }} -O toolchain.run + if [[ ${TOOLCHAIN} == rx-gcc ]]; then + wget --progress=dot:giga ${TOOLCHAIN_URL} -O toolchain.run chmod +x toolchain.run - ./toolchain.run -p ~/cache/${{ inputs.toolchain }}/gnurx -y - elif [[ ${{ inputs.toolchain }} == arm-iar ]]; then - wget --progress=dot:giga ${{ inputs.toolchain_url }} -O ~/cache/${{ inputs.toolchain }}/cxarm.deb + ./toolchain.run -p ~/cache/${TOOLCHAIN}/gnurx -y + elif [[ ${TOOLCHAIN} == ft9xx-gcc ]]; then + wget --progress=dot:giga ${TOOLCHAIN_URL} -O ~/cache/${TOOLCHAIN}/ft9xxtoolchain.deb + elif [[ ${TOOLCHAIN} == arm-iar ]]; then + wget --progress=dot:giga https://netstorage.iar.com/FileStore/STANDARD/001/003/926/iar-lmsc-tools_1.8_amd64.deb -O ~/cache/${TOOLCHAIN}/iar-lmsc-tools.deb + wget --progress=dot:giga ${TOOLCHAIN_URL} -O ~/cache/${TOOLCHAIN}/cxarm.deb + elif [[ ${FILE_EXT} == zip ]]; then + curl -L "$TOOLCHAIN_URL" -o toolchain.zip + unzip -q toolchain.zip -d ~/cache/${TOOLCHAIN} + ~/cache/${TOOLCHAIN}/xpack-arm-none-eabi-gcc-14.2.1-1.1/bin/arm-none-eabi-gcc.exe --version + elif [[ ${FILE_EXT} == gz ]]; then + wget --progress=dot:giga ${TOOLCHAIN_URL} -O toolchain.tar.gz + tar -C ~/cache/${TOOLCHAIN} -xaf toolchain.tar.gz else - wget --progress=dot:giga ${{ inputs.toolchain_url }} -O toolchain.tar.gz - tar -C ~/cache/${{ inputs.toolchain }} -xaf toolchain.tar.gz + echo "Unsupported toolchain file extension: ${FILE_EXT}" + exit 1 fi shell: bash - name: Setup Toolchain + env: + TOOLCHAIN: ${{ inputs.toolchain }} run: | - if [[ ${{ inputs.toolchain }} == arm-iar ]]; then - sudo apt-get install -y ~/cache/${{ inputs.toolchain }}/cxarm.deb - echo >> $GITHUB_PATH "/opt/iar/cxarm/arm/bin" + if [[ ${TOOLCHAIN} == arm-iar ]]; then + sudo dpkg -i ~/cache/${TOOLCHAIN}/iar-lmsc-tools.deb + sudo apt install -y ~/cache/${TOOLCHAIN}/cxarm.deb + TOOLCHAIN_PATH="/opt/iar/cxarm/arm/bin" + elif [[ ${TOOLCHAIN} == ft9xx-gcc ]]; then + sudo apt install -y ~/cache/${TOOLCHAIN}/ft9xxtoolchain.deb + TOOLCHAIN_PATH="/opt/ft32/bin" else - echo >> $GITHUB_PATH `echo ~/cache/${{ inputs.toolchain }}/*/bin` + # Find the single toolchain bin directory + TOOLCHAIN_BIN_DIRS=(~/cache/${TOOLCHAIN}/*/bin) + if [[ ${#TOOLCHAIN_BIN_DIRS[@]} -ne 1 ]]; then + echo "Error: Expected exactly one toolchain bin directory, found ${#TOOLCHAIN_BIN_DIRS[@]}" + exit 1 + fi + TOOLCHAIN_PATH="${TOOLCHAIN_BIN_DIRS[0]}" fi + # Convert to native path for Windows compatibility + if [[ "$RUNNER_OS" == "Windows" ]]; then + TOOLCHAIN_PATH=$(cygpath -w "$TOOLCHAIN_PATH") + fi + echo "$TOOLCHAIN_PATH" >> $GITHUB_PATH shell: bash diff --git a/.github/actions/setup_toolchain/espressif/action.yml b/.github/actions/setup_toolchain/espressif/action.yml index b50ffd41d..ec1ff2e91 100644 --- a/.github/actions/setup_toolchain/espressif/action.yml +++ b/.github/actions/setup_toolchain/espressif/action.yml @@ -7,19 +7,21 @@ inputs: toolchain_version: description: 'Toolchain version' required: false - default: 'v5.3.2' + default: 'v5.5.3' runs: using: "composite" steps: - name: Set DOCKER_ESP_IDF + env: + TOOLCHAIN: ${{ inputs.toolchain }} run: | - DOCKER_ESP_IDF=$HOME/cache/${{ inputs.toolchain }}/docker_image.tar + DOCKER_ESP_IDF=$HOME/cache/${TOOLCHAIN}/docker_image.tar echo "DOCKER_ESP_IDF=$DOCKER_ESP_IDF" >> $GITHUB_ENV shell: bash - name: Cache Docker Image - uses: actions/cache@v4 + uses: actions/cache@v5 id: cache-toolchain-espressif with: path: ${{ env.DOCKER_ESP_IDF }} @@ -27,10 +29,12 @@ runs: - name: Pull and Save Docker Image if: steps.cache-toolchain-espressif.outputs.cache-hit != 'true' + env: + TOOLCHAIN_VERSION: ${{ inputs.toolchain_version }} run: | - docker pull espressif/idf:${{ inputs.toolchain_version }} + docker pull espressif/idf:${TOOLCHAIN_VERSION} mkdir -p $(dirname $DOCKER_ESP_IDF) - docker save -o $DOCKER_ESP_IDF espressif/idf:${{ inputs.toolchain_version }} + docker save -o $DOCKER_ESP_IDF espressif/idf:${TOOLCHAIN_VERSION} du -sh $DOCKER_ESP_IDF shell: bash @@ -42,7 +46,9 @@ runs: shell: bash - name: Tag Local Image + env: + TOOLCHAIN_VERSION: ${{ inputs.toolchain_version }} run: | - docker tag espressif/idf:${{ inputs.toolchain_version }} espressif/idf:tinyusb + docker tag espressif/idf:${TOOLCHAIN_VERSION} espressif/idf:tinyusb docker images shell: bash diff --git a/.github/actions/setup_toolchain/toolchain.json b/.github/actions/setup_toolchain/toolchain.json index f7123ef11..85c746356 100644 --- a/.github/actions/setup_toolchain/toolchain.json +++ b/.github/actions/setup_toolchain/toolchain.json @@ -2,8 +2,11 @@ "aarch64-gcc": "https://developer.arm.com/-/media/Files/downloads/gnu-a/10.3-2021.07/binrel/gcc-arm-10.3-2021.07-x86_64-aarch64-none-elf.tar.xz", "arm-clang": "https://github.com/ARM-software/LLVM-embedded-toolchain-for-Arm/releases/download/release-19.1.1/LLVM-ET-Arm-19.1.1-Linux-x86_64.tar.xz", "arm-gcc": "https://github.com/xpack-dev-tools/arm-none-eabi-gcc-xpack/releases/download/v14.2.1-1.1/xpack-arm-none-eabi-gcc-14.2.1-1.1-linux-x64.tar.gz", + "ft9xx-gcc": "https://github.com/Bridgetek/ft32-toolchain-linux/releases/download/v2.7.6/ft9xxtoolchain_2.7.6_amd64.deb", + "arm-gcc-macos-latest": "https://github.com/xpack-dev-tools/arm-none-eabi-gcc-xpack/releases/download/v14.2.1-1.1/xpack-arm-none-eabi-gcc-14.2.1-1.1-darwin-arm64.tar.gz", + "arm-gcc-windows-latest": "https://github.com/xpack-dev-tools/arm-none-eabi-gcc-xpack/releases/download/v14.2.1-1.1/xpack-arm-none-eabi-gcc-14.2.1-1.1-win32-x64.zip", "msp430-gcc": "http://software-dl.ti.com/msp430/msp430_public_sw/mcu/msp430/MSPGCC/9_2_0_0/export/msp430-gcc-9.2.0.50_linux64.tar.bz2", "riscv-gcc": "https://github.com/xpack-dev-tools/riscv-none-elf-gcc-xpack/releases/download/v13.2.0-2/xpack-riscv-none-elf-gcc-13.2.0-2-linux-x64.tar.gz", "rx-gcc": "https://github.com/hathach/rx_device/releases/download/0.0.1/gcc-8.3.0.202411-GNURX-ELF.run", - "arm-iar": "https://netstorage.iar.com/FileStore/STANDARD/001/003/583/cxarm-9.60.4.deb" + "arm-iar": "https://netstorage.iar.com/FileStore/STANDARD/001/003/723/cxarm-9.70.1.deb" } diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md deleted file mode 100644 index 572b8d6f5..000000000 --- a/.github/copilot-instructions.md +++ /dev/null @@ -1,133 +0,0 @@ -# TinyUSB -TinyUSB is an open-source cross-platform USB Host/Device stack for embedded systems, designed to be memory-safe with no dynamic allocation and thread-safe with all interrupt events deferred to non-ISR task functions. - -Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here. - -## Working Effectively - -### Bootstrap and Build Setup -- Install ARM GCC toolchain: `sudo apt-get update && sudo apt-get install -y gcc-arm-none-eabi` -- Fetch core dependencies: `python3 tools/get_deps.py` -- takes <1 second. NEVER CANCEL. -- For specific board families: `python3 tools/get_deps.py FAMILY_NAME` (e.g., rp2040, stm32f4) -- Dependencies are cached in `lib/` and `hw/mcu/` directories - -### Build Examples -Choose ONE of these approaches: - -**Option 1: Individual Example with CMake (RECOMMENDED)** -```bash -cd examples/device/cdc_msc -mkdir -p build && cd build -cmake -DBOARD=stm32f407disco -DCMAKE_BUILD_TYPE=MinSizeRel .. -cmake --build . -j4 -``` --- takes 1-2 seconds. NEVER CANCEL. Set timeout to 5+ minutes. - -**Option 2: Individual Example with Make** -```bash -cd examples/device/cdc_msc -make BOARD=stm32f407disco all -``` --- takes 2-3 seconds. NEVER CANCEL. Set timeout to 5+ minutes. - -**Option 3: All Examples for a Board** -```bash -python3 tools/build.py -b BOARD_NAME -``` --- takes 15-20 seconds, may have some objcopy failures that are non-critical. NEVER CANCEL. Set timeout to 30+ minutes. - -### Unit Testing -- Install Ceedling: `sudo gem install ceedling` -- Run all unit tests: `cd test/unit-test && ceedling` -- takes 4 seconds. NEVER CANCEL. Set timeout to 10+ minutes. -- Tests use Unity framework with CMock for mocking - -### Documentation -- Install requirements: `pip install -r docs/requirements.txt` -- Build docs: `cd docs && sphinx-build -b html . _build` -- takes 2-3 seconds. NEVER CANCEL. Set timeout to 10+ minutes. - -### Code Quality and Validation -- Format code: `clang-format -i path/to/file.c` (uses `.clang-format` config) -- Check spelling: `pip install codespell && codespell` (uses `.codespellrc` config) -- Pre-commit hooks validate unit tests and code quality automatically - -## Validation - -### ALWAYS Run These After Making Changes -1. **Pre-commit validation** (RECOMMENDED): `pre-commit run --all-files` - - Install pre-commit: `pip install pre-commit && pre-commit install` - - Runs all quality checks, unit tests, spell checking, and formatting - - Takes 10-15 seconds. NEVER CANCEL. Set timeout to 15+ minutes. -2. **Build validation**: Build at least one example that exercises your changes - ```bash - cd examples/device/cdc_msc - make BOARD=stm32f407disco all - ``` - -### Manual Testing Scenarios -- **Device examples**: Cannot be fully tested without real hardware, but must build successfully -- **Unit tests**: Exercise core stack functionality - ALL tests must pass -- **Build system**: Must be able to build examples for multiple board families - -### Board Selection for Testing -- **STM32F4**: `stm32f407disco` - no external SDK required, good for testing -- **RP2040**: `pico_sdk` - requires Pico SDK, commonly used -- **Other families**: Check `hw/bsp/FAMILY/boards/` for available boards - -## Common Tasks and Time Expectations - -### Repository Structure Quick Reference -``` -├── src/ # Core TinyUSB stack -│ ├── class/ # USB device classes (CDC, HID, MSC, Audio, etc.) -│ ├── portable/ # MCU-specific drivers (organized by vendor) -│ ├── device/ # USB device stack core -│ ├── host/ # USB host stack core -│ └── common/ # Shared utilities (FIFO, etc.) -├── examples/ # Example applications -│ ├── device/ # Device examples (cdc_msc, hid_generic, etc.) -│ ├── host/ # Host examples -│ └── dual/ # Dual-role examples -├── hw/bsp/ # Board Support Packages -│ └── FAMILY/boards/ # Board-specific configurations -├── test/unit-test/ # Unit tests using Ceedling -├── tools/ # Build and utility scripts -└── docs/ # Sphinx documentation -``` - -### Build Time Reference -- **Dependency fetch**: <1 second -- **Single example build**: 1-3 seconds -- **Unit tests**: ~4 seconds -- **Documentation build**: ~2.5 seconds -- **Full board examples**: 15-20 seconds -- **Toolchain installation**: 2-5 minutes (one-time) - -### Key Files to Know -- `tools/get_deps.py`: Manages dependencies for MCU families -- `tools/build.py`: Builds multiple examples, supports make/cmake -- `src/tusb.h`: Main TinyUSB header file -- `src/tusb_config.h`: Configuration template -- `examples/device/cdc_msc/`: Most commonly used example for testing -- `test/unit-test/project.yml`: Ceedling test configuration - -### Debugging Build Issues -- **Missing compiler**: Install `gcc-arm-none-eabi` package -- **Missing dependencies**: Run `python3 tools/get_deps.py FAMILY` -- **Board not found**: Check `hw/bsp/FAMILY/boards/` for valid board names -- **objcopy errors**: Often non-critical in full builds, try individual example builds - -### Working with USB Device Classes -- **CDC (Serial)**: `src/class/cdc/` - Virtual serial port -- **HID**: `src/class/hid/` - Human Interface Device (keyboard, mouse, etc.) -- **MSC**: `src/class/msc/` - Mass Storage Class (USB drive) -- **Audio**: `src/class/audio/` - USB Audio Class -- Each class has device (`*_device.c`) and host (`*_host.c`) implementations - -### MCU Family Support -- **STM32**: Largest support (F0, F1, F2, F3, F4, F7, G0, G4, H7, L4, U5, etc.) -- **Raspberry Pi**: RP2040, RP2350 with PIO-USB host support -- **NXP**: iMXRT, Kinetis, LPC families -- **Microchip**: SAM D/E/G/L families -- Check `hw/bsp/` for complete list and `docs/reference/boards.rst` for details - -Remember: TinyUSB is designed for embedded systems - builds are fast, tests are focused, and the codebase is optimized for resource-constrained environments. diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 000000000..6c7aa7e7d --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,77 @@ +# Path-based auto-labeling for USB IP / port drivers. +# Maps changed dcd/hcd files under src/portable/ to their "Port <ip>" label. +# Consumed by actions/labeler (see .github/workflows/labeler.yml -> label-port job). + +"Port DWC2": + - changed-files: + - any-glob-to-any-file: 'src/portable/synopsys/dwc2/**' + +"Port EHCI": + - changed-files: + - any-glob-to-any-file: 'src/portable/ehci/**' + +"Port OHCI": + - changed-files: + - any-glob-to-any-file: 'src/portable/ohci/**' + +"Port FSDev": + - changed-files: + - any-glob-to-any-file: 'src/portable/st/stm32_fsdev/**' + +"Port ChipIdea": + - changed-files: + - any-glob-to-any-file: 'src/portable/chipidea/**' + +"Port NXP IP3511": + - changed-files: + - any-glob-to-any-file: 'src/portable/nxp/lpc_ip3511/**' + +"Port NXP IP3516": + - changed-files: + - any-glob-to-any-file: 'src/portable/nxp/lpc_ip3516/**' + +"Port MUSB": + - changed-files: + - any-glob-to-any-file: + - 'src/portable/mentor/musb/**' + - 'src/portable/sunxi/**' + +"Port RUSB2": + - changed-files: + - any-glob-to-any-file: 'src/portable/renesas/rusb2/**' + +"Port WCH USBFS": + - changed-files: + - any-glob-to-any-file: 'src/portable/wch/*usbfs*' + +"Port WCH USBHS": + - changed-files: + - any-glob-to-any-file: 'src/portable/wch/*usbhs*' + +"Port MAX3421": + - changed-files: + - any-glob-to-any-file: 'src/portable/analog/max3421/**' + +"Port SAMD": + - changed-files: + - any-glob-to-any-file: 'src/portable/microchip/samd/**' + +"Port SAMG": + - changed-files: + - any-glob-to-any-file: 'src/portable/microchip/samg/**' + +"Port nRF": + - changed-files: + - any-glob-to-any-file: 'src/portable/nordic/nrf5x/**' + +"Port Nuvoton": + - changed-files: + - any-glob-to-any-file: 'src/portable/nuvoton/**' + +"Port RP2": + - changed-files: + - any-glob-to-any-file: 'src/portable/raspberrypi/**' + +"Port MSP430": + - changed-files: + - any-glob-to-any-file: 'src/portable/ti/msp430x5xx/**' diff --git a/.github/membrowse_pr_message.j2 b/.github/membrowse_pr_message.j2 new file mode 100644 index 000000000..fbba1ee78 --- /dev/null +++ b/.github/membrowse_pr_message.j2 @@ -0,0 +1,38 @@ +{#- Top 10 targets with biggest memory changes + project dashboard link -#} +{% set section_columns = ['.text', '.rodata', '.data', '.bss'] -%} +{#- --- Compute per-target total absolute delta and collect changed targets --- -#} +{% set changed = [] -%} +{% for target in targets -%} +{% if target.has_changes -%} +{% set ns = namespace(total_delta=0, total_current=0) -%} +{% for region in target.regions -%} +{% set ns.total_delta = ns.total_delta + region.delta -%} +{% set ns.total_current = ns.total_current + region.used_size -%} +{% endfor -%} +{% set total_old = ns.total_current - ns.total_delta -%} +{% set pct = (ns.total_delta / total_old * 100) if total_old > 0 else 0 -%} +{% set abs_pct = (ns.total_delta | abs) if total_old == 0 else (pct | abs) -%} +{% set _ = changed.append({'target': target, 'total_current': ns.total_current, 'total_old': total_old, 'total_delta': ns.total_delta, 'pct': pct, 'abs_pct': abs_pct}) -%} +{% endif -%} +{% endfor -%} +{#- --- Sort by absolute percentage change descending and take top 10 --- -#} +{% set sorted_changed = changed | sort(attribute='abs_pct', reverse=true) -%} +{% set top10 = sorted_changed[:10] -%} +{#- --- Render --- -#} +{% if top10 %} +### Top {{ top10 | length }} targets by memory change (%) (out of {{ targets | length }} targets) {% if dashboard_url %} [View Project Dashboard →]({{ dashboard_url }}){% endif %} + +| target | .text | .rodata | .data | .bss | total | % diff | +|--------|-------|---------|-------|------|-------|--------| +{% for info in top10 -%} +{% set target = info.target -%} +{% set section_map = {} -%} +{% for section in target.sections -%} +{% set _ = section_map.update({section.name: section}) -%} +{% endfor -%} +| {% if target.comparison_url %}[{{ target.name }}]({{ target.comparison_url }}){% else %}{{ target.name }}{% endif %} | +{%- for col in section_columns %} {% if col in section_map %}{{ "{:,}".format(section_map[col].old.size) }} → {{ "{:,}".format(section_map[col].size) }} ({{ section_map[col].delta_str }}){% else %}—{% endif %} |{% endfor %} {{ "{:,}".format(info.total_old) }} → {{ "{:,}".format(info.total_current) }} ({% if info.total_delta >= 0 %}+{{ "{:,}".format(info.total_delta) }}{% else %}{{ "{:,}".format(info.total_delta) }}{% endif %}) | {% if info.total_old > 0 %}{% if info.pct >= 0 %}+{% endif %}{{ "%.1f" | format(info.pct) }}%{% else %}N/A{% endif %} | +{% endfor %} +{% else %} +No memory changes detected across {{ targets | length }} target{{ 's' if targets | length != 1 else '' }}.{% if dashboard_url %} [View Project Dashboard →]({{ dashboard_url }}){% endif %} +{% endif -%} diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md deleted file mode 100644 index aa148eb79..000000000 --- a/.github/pull_request_template.md +++ /dev/null @@ -1,5 +0,0 @@ -**Describe the PR** -A clear and concise description of what this PR solve. - -**Additional context** -If applicable, add any other context about the PR and/or screenshots here. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9243c866d..c8c597e50 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,39 +3,46 @@ name: Build on: workflow_dispatch: push: - paths: - - 'src/**' - - 'examples/**' - - 'lib/**' - - 'hw/**' - - 'tools/build.py' - - 'tools/get_deps.py' - - '.github/actions/**' - - '.github/workflows/build.yml' - - '.github/workflows/build_util.yml' - - '.github/workflows/ci_set_matrix.py' + branches: [master] pull_request: - branches: [ master ] - paths: - - 'src/**' - - 'examples/**' - - 'lib/**' - - 'hw/**' - - 'test/hil/**' - - 'tools/build.py' - - 'tools/get_deps.py' - - '.github/actions/**' - - '.github/workflows/build.yml' - - '.github/workflows/build_util.yml' - - '.github/workflows/ci_set_matrix.py' -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + release: + types: [ published ] -env: - HIL_JSON: test/hil/tinyusb.json +concurrency: + group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.sha || github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/master' }} jobs: + # Check if the code changes and we need to run ci build + # Cannot use paths filter in the on-event since we want this workflow to run even when there are no code changes, to register the commit chain + check-paths: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + outputs: + code_changed: ${{ steps.filter.outputs.code }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 2 # Needed for push commit comparison + - uses: dorny/paths-filter@v4 + id: filter + with: + filters: | + code: + - 'src/**' + - 'examples/**' + - 'lib/**' + - 'hw/**' + - 'test/hil/**' + - 'tools/build.py' + - 'tools/get_deps.py' + - '.github/actions/**' + - '.github/workflows/build.yml' + - '.github/workflows/build_util.yml' + - '.github/workflows/ci_set_matrix.py' + set-matrix: runs-on: ubuntu-latest outputs: @@ -43,7 +50,7 @@ jobs: hil_json: ${{ steps.set-matrix-json.outputs.hil_matrix }} steps: - name: Checkout TinyUSB - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Generate matrix json id: set-matrix-json @@ -52,18 +59,19 @@ jobs: MATRIX_JSON=$(python .github/workflows/ci_set_matrix.py) echo "matrix=$MATRIX_JSON" echo "matrix=$MATRIX_JSON" >> $GITHUB_OUTPUT - # hil matrix - HIL_MATRIX_JSON=$(python test/hil/hil_ci_set_matrix.py ${{ env.HIL_JSON }}) + + # HIL matrix (merged from tinyusb + hifiphile configs) + HIL_MATRIX_JSON=$(python test/hil/hil_ci_set_matrix.py test/hil/tinyusb.json test/hil/hfp.json) echo "hil_matrix=$HIL_MATRIX_JSON" echo "hil_matrix=$HIL_MATRIX_JSON" >> $GITHUB_OUTPUT - # --------------------------------------- - # Build CMake: only build on push with one-per-family. - # Full built is done by CircleCI in PR - # --------------------------------------- + # ------------------------------------------------------------------------------ + # CMake build: only one board per family (first alphabetically). Full build is done by CircleCI in PR + # Note: + # For Make and IAR build: will be done on CircleCI only (one random per family as well) + # ------------------------------------------------------------------------------ cmake: - if: github.event_name == 'push' - needs: set-matrix + needs: [ check-paths, set-matrix ] uses: ./.github/workflows/build_util.yml strategy: fail-fast: false @@ -72,86 +80,174 @@ jobs: - 'aarch64-gcc' #- 'arm-clang' - 'arm-gcc' - - 'esp-idf' + #- 'esp-idf' + - 'ft9xx-gcc' - 'msp430-gcc' - 'riscv-gcc' with: build-system: 'cmake' toolchain: ${{ matrix.toolchain }} build-args: ${{ toJSON(fromJSON(needs.set-matrix.outputs.json)[matrix.toolchain]) }} - one-per-family: true + build-options: '--one-first' + upload-metrics: true + upload-artifacts: false + upload-membrowse: true + code-changed: ${{ needs.check-paths.outputs.code_changed == 'true' }} + secrets: inherit - # --------------------------------------- - # Build Make: only build on push with one-per-family - # --------------------------------------- - make: - if: github.event_name == 'push' - needs: set-matrix - uses: ./.github/workflows/build_util.yml - strategy: - fail-fast: false - matrix: - toolchain: - - 'aarch64-gcc' - #- 'arm-clang' - - 'arm-gcc' - - 'msp430-gcc' - - 'riscv-gcc' - - 'rx-gcc' - with: - build-system: 'make' - toolchain: ${{ matrix.toolchain }} - build-args: ${{ toJSON(fromJSON(needs.set-matrix.outputs.json)[matrix.toolchain]) }} - one-per-family: true + code-metrics: + needs: [ check-paths, cmake ] + if: needs.check-paths.outputs.code_changed == 'true' + runs-on: ubuntu-latest + permissions: + pull-requests: write + contents: write + steps: + - name: Checkout TinyUSB + uses: actions/checkout@v6 + with: + fetch-tags: ${{ github.event_name == 'release' }} + + - name: Download Artifacts + uses: actions/download-artifact@v5 + with: + pattern: metrics-* + path: cmake-build + merge-multiple: true + + - name: Aggregate Code Metrics + run: | + python tools/get_deps.py + python tools/metrics.py combine -j -m -f tinyusb/src cmake-build/*/metrics.json + + - name: Upload Metrics Artifact + if: github.event_name == 'push' || github.event_name == 'release' + uses: actions/upload-artifact@v7 + with: + name: metrics-tinyusb + path: metrics.json + + - name: Download Base Branch Metrics + if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' + uses: dawidd6/action-download-artifact@v11 + with: + workflow: build.yml + workflow_conclusion: '' + branch: ${{ github.base_ref }} + name: metrics-tinyusb + path: base-metrics + continue-on-error: true + + - name: Download Previous Release Asset + if: github.event_name == 'release' + env: + GH_TOKEN: ${{ github.token }} + run: | + PREV_TAG=$(git tag --sort=-creatordate | head -n 2 | tail -n 1) + echo "Previous Release: $PREV_TAG" + echo "PREV_TAG=$PREV_TAG" >> $GITHUB_ENV + + mkdir -p base-metrics + gh release download $PREV_TAG -p metrics.json -D base-metrics || echo "No metrics.json found in $PREV_TAG release" + + - name: Compare with Base Branch + if: github.event_name != 'push' + run: | + if [ -f base-metrics/metrics.json ]; then + python tools/metrics.py compare -m -f tinyusb/src base-metrics/metrics.json metrics.json + cat metrics_compare.md + else + echo "No base metrics found, skipping comparison" + cp metrics.md metrics_compare.md + fi + + - name: Upload Release Assets + if: github.event_name == 'release' + env: + GH_TOKEN: ${{ github.token }} + run: | + CURR_TAG=${{ github.event.release.tag_name }} + COMPARE_FILE="metrics_compare_${CURR_TAG}-${PREV_TAG}.md" + mv metrics_compare.md $COMPARE_FILE + gh release upload $CURR_TAG metrics.json $COMPARE_FILE + + - name: Upload Metrics Comment Artifact + if: github.event_name == 'pull_request' + uses: actions/upload-artifact@v7 + with: + name: metrics-comment + path: | + metrics_compare.md + metrics.json + + - name: Post Code Metrics as PR Comment + if: (github.event_name == 'workflow_dispatch') || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false) + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: code-metrics + path: metrics_compare.md # --------------------------------------- - # Build IAR - # Since IAR Token secret is not passed to forked PR, only build non-forked PR with make. - # cmake is built by circle-ci. Due to IAR limit capacity, only build oe per family + # Keep the metrics baseline available on no-code-change pushes + # The code-metrics job only runs (and uploads metrics-tinyusb) when code changed, so a + # workflow/docs-only push to master would leave the latest run without a baseline for PRs + # to compare against. Carry the previous artifact forward so the baseline is never missing. # --------------------------------------- - arm-iar: - if: false # disable for now since we got reach capacity limit too often - #if: github.event_name == 'push' && github.repository_owner == 'hathach' - needs: set-matrix - uses: ./.github/workflows/build_util.yml - secrets: inherit - strategy: - fail-fast: false - matrix: - build-system: - - 'make' - with: - build-system: ${{ matrix.build-system }} - toolchain: 'arm-iar' - build-args: ${{ toJSON(fromJSON(needs.set-matrix.outputs.json)['arm-iar']) }} - one-per-family: true + metrics-carry-forward: + needs: [ check-paths ] + if: github.event_name == 'push' && needs.check-paths.outputs.code_changed != 'true' + runs-on: ubuntu-latest + steps: + - name: Download previous metrics baseline from this branch + uses: dawidd6/action-download-artifact@v11 + with: + workflow: build.yml + workflow_conclusion: '' # any conclusion, matching the PR-side baseline download + search_artifacts: true # scan back past runs that lack the artifact (e.g. earlier no-code pushes) + branch: ${{ github.ref_name }} + name: metrics-tinyusb + path: . + if_no_artifact_found: warn + continue-on-error: true # best-effort: never make a no-code push red + + - name: Re-publish baseline so the latest run keeps it + if: hashFiles('metrics.json') != '' + uses: actions/upload-artifact@v7 + with: + name: metrics-tinyusb + path: metrics.json # --------------------------------------- - # Build Make on Windows/MacOS + # Build Make/CMake on Windows/MacOS # --------------------------------------- - make-os: - if: github.event_name == 'pull_request' + build-os: + needs: [ check-paths ] + if: needs.check-paths.outputs.code_changed == 'true' uses: ./.github/workflows/build_util.yml strategy: fail-fast: false matrix: - os: [windows-latest, macos-latest] + os: [ windows-latest, macos-latest ] + build-system: [ 'make', 'cmake' ] with: os: ${{ matrix.os }} - build-system: 'make' - toolchain: 'arm-gcc' - build-args: '["stm32h7"]' - one-per-family: true + build-system: ${{ matrix.build-system }} + toolchain: 'arm-gcc-${{ matrix.os }}' + build-args: '["stm32h7rs"]' + build-options: '--one-random' # --------------------------------------- # Zephyr # --------------------------------------- zephyr: - if: github.event_name == 'push' + needs: [ check-paths ] + # skip zephyr build due to failed build, fix later + if: false + #if: needs.check-paths.outputs.code_changed == 'true' runs-on: ubuntu-latest steps: - name: Checkout TinyUSB - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup Zephyr project uses: zephyrproject-rtos/action-zephyr-setup@v1 @@ -161,30 +257,28 @@ jobs: - name: Build run: | - west build -b pca10056 -d examples/device/cdc_msc/build examples/device/cdc_msc -- -DRTOS=zephyr - west build -b pca10056 -d examples/device/msc_dual_lun/build examples/device/msc_dual_lun -- -DRTOS=zephyr + west build -b nrf52840dk -d examples/device/cdc_msc/build examples/device/cdc_msc -- -DRTOS=zephyr + west build -b nrf52840dk -d examples/device/msc_dual_lun/build examples/device/msc_dual_lun -- -DRTOS=zephyr # --------------------------------------- # Hardware in the loop (HIL) - # Run on PR only (hil-tinyusb), hil-hfp only run on non-forked PR + # Run on PR only (hil-tinyusb), hil-hfp-iar only run on non-forked PR # --------------------------------------- hil-build: - if: | - github.repository_owner == 'hathach' && - (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch') - needs: set-matrix + needs: [ check-paths, set-matrix ] + if: needs.check-paths.outputs.code_changed == 'true' && github.repository_owner == 'hathach' uses: ./.github/workflows/build_util.yml strategy: fail-fast: false matrix: toolchain: - 'arm-gcc' + - 'riscv-gcc' - 'esp-idf' with: build-system: 'cmake' toolchain: ${{ matrix.toolchain }} build-args: ${{ toJSON(fromJSON(needs.set-matrix.outputs.hil_json)[matrix.toolchain]) }} - one-per-family: true upload-artifacts: true # --------------------------------------- @@ -192,12 +286,36 @@ jobs: # self-hosted on local VM, for attached hardware checkout HIL_JSON # --------------------------------------- hil-tinyusb: - if: | - github.repository_owner == 'hathach' && - (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch') needs: hil-build - runs-on: [self-hosted, X64, hathach, hardware-in-the-loop] + name: hil-tinyusb (${{ matrix.display }}) + strategy: + fail-fast: false + matrix: + include: + - display: tinyusb.json + runner: [ self-hosted, X64, hathach, hardware-in-the-loop ] + hil_json: test/hil/tinyusb.json + - display: hfp.json + runner: [ self-hosted, Linux, X64, hifiphile ] + hil_json: test/hil/hfp.json + runs-on: ${{ matrix.runner }} + env: + HIL_JSON: ${{ matrix.hil_json }} steps: + - name: Set HIL report dir (sibling of workspace; persists across run attempts) + run: echo "HIL_REPORT_DIR=$(dirname "$GITHUB_WORKSPACE")/hil-report" >> "$GITHUB_ENV" + + - name: Get Skip Boards from previous run + if: github.run_attempt != '1' + run: | + if [ -f "${{ env.HIL_JSON }}.skip" ]; then + SKIP_BOARDS=$(cat "${{ env.HIL_JSON }}.skip") + else + SKIP_BOARDS="" + fi + echo "SKIP_BOARDS=$SKIP_BOARDS" + echo "SKIP_BOARDS=$SKIP_BOARDS" >> $GITHUB_ENV + - name: Clean workspace run: | echo "Cleaning up for the first run" @@ -205,53 +323,56 @@ jobs: mkdir -p "${{ github.workspace }}" - name: Checkout TinyUSB - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Download Artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: + pattern: binaries-* path: cmake-build merge-multiple: true - - name: Cache skip list - uses: actions/cache@v4 - with: - path: ${{ env.HIL_JSON }}.skip - key: hil-skip-${{ github.run_id }}-${{ github.run_attempt }} - restore-keys: | - hil-skip-${{ github.run_id }}- - - name: Test on actual hardware run: | - ls cmake-build/ - - # Skip boards that passed with previous run, file is generated by hil_test.py - SKIP_BOARDS="" - if [ -f ${{ env.HIL_JSON }}.skip ]; then - SKIP_BOARDS=$(cat "${HIL_JSON}.skip") - fi - echo "SKIP_BOARDS=$SKIP_BOARDS" + python3 test/hil/hil_test.py ${{ env.HIL_JSON }} $SKIP_BOARDS || \ + (if [ -f "${{ env.HIL_JSON }}.skip" ]; then + SKIP_BOARDS=$(cat "${{ env.HIL_JSON }}.skip") + echo "Re-running with SKIP_BOARDS=$SKIP_BOARDS" + python3 test/hil/hil_test.py ${{ env.HIL_JSON }} $SKIP_BOARDS + else + exit 1 + fi) - python3 test/hil/hil_test.py ${{ env.HIL_JSON }} $SKIP_BOARDS + - name: Upload HIL report + if: always() && github.event_name == 'pull_request' + uses: actions/upload-artifact@v7 + with: + name: hil-report-${{ matrix.display }} + path: ${{ env.HIL_REPORT_DIR }}/hil_report.md + if-no-files-found: ignore + overwrite: true # --------------------------------------- # Hardware in the loop (HIL) # self-hosted by HFP, build with IAR toolchain, for attached hardware checkout test/hil/hfp.json # Since IAR Token secret is not passed to forked PR, only build non-forked PR # --------------------------------------- - hil-hfp: + hil-hfp-iar: + needs: [ check-paths ] if: | + needs.check-paths.outputs.code_changed == 'true' && github.repository_owner == 'hathach' && - github.event.pull_request.head.repo.fork == false && - (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch') - runs-on: [self-hosted, Linux, X64, hifiphile] + !(github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true) + runs-on: [ self-hosted, Linux, X64, hifiphile ] + timeout-minutes: 30 env: IAR_LMS_BEARER_TOKEN: ${{ secrets.IAR_LMS_BEARER_TOKEN }} + PYTHONUNBUFFERED: '1' steps: - name: Clean workspace run: | echo "Cleaning up previous run" - rm -rf "${{ github.workspace }}"3 + rm -rf "${{ github.workspace }}" mkdir -p "${{ github.workspace }}" - name: Toolchain version @@ -259,7 +380,7 @@ jobs: iccarm --version - name: Checkout TinyUSB - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Get build boards run: | @@ -272,7 +393,25 @@ jobs: run: python3 tools/get_deps.py $BUILD_ARGS - name: Build - run: python3 tools/build.py -j 4 --toolchain iar $BUILD_ARGS + run: | + # Each variant carries its own --build-name/--cflag, which are global to a + # single build.py invocation — so build one matrix entry at a time rather + # than joining them (joining would leak a variant's flags onto every board). + readarray -t ENTRIES < <(python test/hil/hil_ci_set_matrix.py test/hil/hfp.json | jq -r '.["arm-gcc"][]') + for entry in "${ENTRIES[@]}"; do + echo "+ tools/build.py --toolchain iar $entry" + python3 tools/build.py --toolchain iar $entry + done - name: Test on actual hardware (hardware in the loop) - run: python3 test/hil/hil_test.py hfp.json + run: | + python3 test/hil/hil_test.py hfp.json + + - name: Upload HIL report + if: always() && github.event_name == 'pull_request' + uses: actions/upload-artifact@v7 + with: + name: hil-report-hfp-iar + path: hil_report.md + if-no-files-found: ignore + overwrite: true diff --git a/.github/workflows/build_util.yml b/.github/workflows/build_util.yml index a2c96f3c0..2532caebe 100644 --- a/.github/workflows/build_util.yml +++ b/.github/workflows/build_util.yml @@ -3,6 +3,10 @@ name: Reusable build util on: workflow_call: inputs: + os: + required: false + type: string + default: 'ubuntu-latest' build-system: required: true type: string @@ -12,18 +16,26 @@ on: build-args: required: true type: string - one-per-family: + build-options: + required: false + default: '' + type: string + upload-artifacts: required: false default: false type: boolean - upload-artifacts: + upload-metrics: required: false default: false type: boolean - os: + upload-membrowse: required: false - type: string - default: 'ubuntu-latest' + default: false + type: boolean + code-changed: + required: false + default: true + type: boolean jobs: family: @@ -34,7 +46,9 @@ jobs: arg: ${{ fromJSON(inputs.build-args) }} steps: - name: Checkout TinyUSB - uses: actions/checkout@v4 + uses: actions/checkout@v6 + with: + fetch-depth: ${{ !inputs.upload-membrowse && 1 || 0 }} - name: Setup Toolchain id: setup-toolchain @@ -47,32 +61,45 @@ jobs: with: arg: ${{ matrix.arg }} - - name: Set build one-per-family option - id: set-one-per-family - run: | - if [[ "${{ inputs.one-per-family }}" == "true" ]]; then - BUILD_OPTION="--one-per-family" - fi - echo "build_option=$BUILD_OPTION" - echo "build_option=$BUILD_OPTION" >> $GITHUB_OUTPUT - shell: bash - - name: Build + if: ${{ inputs.code-changed }} env: IAR_LMS_BEARER_TOKEN: ${{ secrets.IAR_LMS_BEARER_TOKEN }} run: | if [ "${{ inputs.toolchain }}" == "esp-idf" ]; then - docker run --rm -v $PWD:/project -w /project espressif/idf:tinyusb python tools/build.py ${{ matrix.arg }} + docker run --rm -e MEMBROWSE_API_KEY="$MEMBROWSE_API_KEY" -e CI="$CI" -v $PWD:/project -w /project espressif/idf:tinyusb python tools/build.py --target all ${{ matrix.arg }} else - python tools/build.py -s ${{ inputs.build-system }} ${{ steps.setup-toolchain.outputs.build_option }} ${{ steps.set-one-per-family.outputs.build_option }} ${{ matrix.arg }} + BUILD_PY_ARGS="-s ${{ inputs.build-system }} ${{ steps.setup-toolchain.outputs.build_option }} ${{ inputs.build-options }} --target all" + if [ "${{ inputs.upload-metrics }}" = "true" ]; then + BUILD_PY_ARGS="$BUILD_PY_ARGS --target tinyusb_metrics" + fi + python tools/build.py $BUILD_PY_ARGS ${{ matrix.arg }} fi shell: bash + - name: Membrowse Upload + if: inputs.toolchain != 'esp-idf' && inputs.upload-membrowse == true + continue-on-error: true + env: + MEMBROWSE_API_KEY: ${{ secrets.MEMBROWSE_API_KEY }} + run: | + # if code-changed is false --> there is no elf -> membrowse target upload with --identical flag + BUILD_PY_ARGS="-s ${{ inputs.build-system }} ${{ steps.setup-toolchain.outputs.build_option }} ${{ inputs.build-options }}" + python tools/build.py $BUILD_PY_ARGS --target examples-membrowse-upload -j 1 ${{ matrix.arg }} + shell: bash + + - name: Upload Artifacts for Metrics + if: inputs.upload-metrics == true && inputs.code-changed == true + uses: actions/upload-artifact@v7 + with: + name: metrics-${{ matrix.arg }} + path: cmake-build/cmake-build-*/metrics.json + - name: Upload Artifacts for Hardware Testing - if: ${{ inputs.upload-artifacts }} - uses: actions/upload-artifact@v4 + if: inputs.upload-artifacts == true && inputs.code-changed == true + uses: actions/upload-artifact@v7 with: - name: ${{ matrix.arg }} + name: binaries-${{ matrix.arg }} path: | cmake-build/cmake-build-*/*/*/*.elf cmake-build/cmake-build-*/*/*/*.bin @@ -81,3 +108,4 @@ jobs: cmake-build/cmake-build-*/*/*/partition_table/partition-table.bin cmake-build/cmake-build-*/*/*/config.env cmake-build/cmake-build-*/*/*/flash_args + cmake-build/hw/mcu/**/*.ld diff --git a/.github/workflows/ci_set_matrix.py b/.github/workflows/ci_set_matrix.py index e53998c66..dc0d3871f 100755 --- a/.github/workflows/ci_set_matrix.py +++ b/.github/workflows/ci_set_matrix.py @@ -8,6 +8,7 @@ toolchain_list = [ "arm-iar", "arm-gcc", "esp-idf", + "ft9xx-gcc", "msp430-gcc", "riscv-gcc", "rx-gcc" @@ -15,39 +16,80 @@ toolchain_list = [ # family: [supported toolchain] family_list = { - "at32f402_405 at32f403a_407 at32f413 at32f415 at32f423 at32f425 at32f435_437": ["arm-gcc"], + "at32f402_405": ["arm-gcc"], + "at32f403a_407": ["arm-gcc"], + "at32f413": ["arm-gcc"], + "at32f415": ["arm-gcc"], + "at32f423": ["arm-gcc"], + "at32f425": ["arm-gcc"], + "at32f435_437": ["arm-gcc"], + "at32f45x": ["arm-gcc"], "broadcom_32bit": ["arm-gcc"], "broadcom_64bit": ["aarch64-gcc"], - "ch32v10x ch32v20x ch32v30x fomu gd32vf103": ["riscv-gcc"], + "ch32f20x": ["arm-gcc"], + "ch32v10x": ["riscv-gcc"], + "ch32v20x": ["riscv-gcc"], + "ch32v30x": ["riscv-gcc"], + "ch583": ["riscv-gcc"], "da1469x": ["arm-gcc"], + "fomu": ["riscv-gcc"], + "ft9xx": ["ft9xx-gcc"], + "gd32vf103": ["riscv-gcc"], + "hpmicro": ["riscv-gcc"], "imxrt": ["arm-gcc", "arm-clang"], - "kinetis_k kinetis_kl kinetis_k32l2": ["arm-gcc", "arm-clang"], - "lpc11 lpc13 lpc15": ["arm-gcc", "arm-clang"], - "lpc17 lpc18 lpc40 lpc43": ["arm-gcc", "arm-clang"], - "lpc51 lpc54 lpc55": ["arm-gcc", "arm-clang"], + "kinetis_k": ["arm-gcc"], + "kinetis_k32l": ["arm-gcc"], + "kinetis_kl": ["arm-gcc"], + "lpc11": ["arm-gcc", "arm-clang"], + "lpc13": ["arm-gcc", "arm-clang"], + "lpc15": ["arm-gcc", "arm-clang"], + "lpc17": ["arm-gcc", "arm-clang"], + "lpc18": ["arm-gcc", "arm-clang"], + "lpc40": ["arm-gcc", "arm-clang"], + "lpc43": ["arm-gcc", "arm-clang"], + "lpc51": ["arm-gcc", "arm-clang"], + "lpc54": ["arm-gcc", "arm-clang"], + "lpc55": ["arm-gcc", "arm-clang"], "maxim": ["arm-gcc"], "mcx": ["arm-gcc"], "mm32": ["arm-gcc"], "msp430": ["msp430-gcc"], - "msp432e4 tm4c": ["arm-gcc"], + "msp432e4": ["arm-gcc"], "nrf": ["arm-gcc", "arm-clang"], + "nuc100_120": ["arm-gcc"], + "nuc121_125": ["arm-gcc"], + "nuc126": ["arm-gcc"], + "nuc505": ["arm-gcc"], "ra": ["arm-gcc"], "rp2040": ["arm-gcc"], + "rw61x": ["arm-gcc"], "rx": ["rx-gcc"], - "samd11 saml2x": ["arm-gcc", "arm-clang"], - "samd21": ["arm-gcc", "arm-clang"], - "samd5x_e5x samg": ["arm-gcc", "arm-clang"], - "stm32c0 stm32f0 stm32f1 stm32f2 stm32f3": ["arm-gcc", "arm-clang", "arm-iar"], + "samd11": ["arm-gcc", "arm-clang"], + "samd2x_l2x": ["arm-gcc", "arm-clang"], + "samd5x_e5x": ["arm-gcc", "arm-clang"], + "samg": ["arm-gcc", "arm-clang"], + "stm32c0": ["arm-gcc", "arm-clang", "arm-iar"], + "stm32c5": ["arm-gcc", "arm-clang", "arm-iar"], + "stm32f0": ["arm-gcc", "arm-clang", "arm-iar"], + "stm32f1": ["arm-gcc", "arm-clang", "arm-iar"], + "stm32f2": ["arm-gcc", "arm-clang", "arm-iar"], + "stm32f3": ["arm-gcc", "arm-clang", "arm-iar"], "stm32f4": ["arm-gcc", "arm-clang", "arm-iar"], "stm32f7": ["arm-gcc", "arm-clang", "arm-iar"], - "stm32g0 stm32g4 stm32h5": ["arm-gcc", "arm-clang", "arm-iar"], - "stm32h7 stm32h7rs": ["arm-gcc", "arm-clang", "arm-iar"], - "stm32l0 stm32l4": ["arm-gcc", "arm-clang", "arm-iar"], + "stm32g0": ["arm-gcc", "arm-clang", "arm-iar"], + "stm32g4": ["arm-gcc", "arm-clang", "arm-iar"], + "stm32h5": ["arm-gcc", "arm-clang", "arm-iar"], + "stm32h7": ["arm-gcc", "arm-clang", "arm-iar"], + "stm32h7rs": ["arm-gcc", "arm-clang", "arm-iar"], + "stm32l0": ["arm-gcc", "arm-clang", "arm-iar"], + "stm32l4": ["arm-gcc", "arm-clang", "arm-iar"], "stm32n6": ["arm-gcc"], - "stm32u0 stm32u5 stm32wb": ["arm-gcc", "arm-clang", "arm-iar"], - "stm32wba": ["arm-gcc", "arm-clang"], + "stm32u0": ["arm-gcc", "arm-clang", "arm-iar"], + "stm32u5": ["arm-gcc", "arm-clang", "arm-iar"], + "stm32wb": ["arm-gcc", "arm-clang", "arm-iar"], + "stm32wba": ["arm-gcc", "arm-clang", "arm-iar"], + "tm4c": ["arm-gcc"], "xmc4000": ["arm-gcc"], - "-bespressif_s2_devkitc": ["esp-idf"], # S3, P4 will be built by hil test # "-bespressif_s3_devkitm": ["esp-idf"], # "-bespressif_p4_function_ev": ["esp-idf"], diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml index d7f1fc066..ff75a8ba6 100644 --- a/.github/workflows/cifuzz.yml +++ b/.github/workflows/cifuzz.yml @@ -29,7 +29,7 @@ jobs: fuzz-seconds: 400 - name: Upload Crash - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() && steps.build.outcome == 'success' with: name: artifacts diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml new file mode 100644 index 000000000..59019616f --- /dev/null +++ b/.github/workflows/claude-code-review.yml @@ -0,0 +1,60 @@ +name: Claude Code Review + +on: + pull_request: + # opened/reopened/ready_for_review -> first auto review + # synchronize -> auto re-review on new pushes + # + # NOTE: pull_request (not _target) means fork PRs get a read-only GITHUB_TOKEN + # and NO repository secrets (CLAUDE_CODE_OAUTH_TOKEN), so they cannot be + # auto-reviewed. The job condition below skips them cleanly -> use @claude on + # those. Same-repo branches (yours or write-access contributors) auto-review. + types: [opened, synchronize, reopened, ready_for_review] + +jobs: + claude-review: + # Skip drafts, and skip fork PRs (no secrets -> would only fail noisily) + if: > + github.event.pull_request.draft == false && + github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + issues: write # Claude posts the review comment via the issues API + id-token: write + actions: read # Required for Claude to read CI results on PRs + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 1 + + - name: Run Claude Code Review + id: claude-review + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + # Allow claude[bot]'s own pushes to be handled gracefully (skip) instead + # of erroring out the workflow + allowed_bots: 'claude' + # Pairs with the actions: read permission so Claude can read CI results + additional_permissions: | + actions: read + plugin_marketplaces: 'https://github.com/anthropics/claude-code.git' + plugins: 'code-review@claude-code-plugins' + # Post/update a single summary comment every run, so a clean review + # ("no issues found") is still visible instead of posting nothing. + use_sticky_comment: true + # --comment makes the code-review command post its findings to the PR. + # Without it the command only prints the review to the Actions log. + prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }} --comment' + # TEMPORARY: expose the full Claude transcript in the Actions log for + # debugging. Revert to remove once done. + show_full_output: true + claude_args: | + --max-turns 50 + --model claude-opus-4-8 + --effort max + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index a6ea7e396..66e36897c 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -6,27 +6,37 @@ on: pull_request_review_comment: types: [created] issues: - types: [opened, assigned] + # only "opened" — an issue's author_association gates the summon below; + # "assigned" would gate on the issue author, not the assigner, so a + # maintainer assigning an outsider's issue would be wrongly skipped. + types: [opened] pull_request_review: types: [submitted] jobs: claude: + # Only trusted actors (repo owner/member/collaborator) may summon @claude, so the + # write-scoped token and OAuth secret are never issued for an outside contributor's + # comment on this public repo. Defense-in-depth on top of the action's own check. if: | - (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || - (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude') && + contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude') && + contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude') && + contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.review.author_association)) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')) && + contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.issue.author_association)) runs-on: ubuntu-latest permissions: - contents: read - pull-requests: read - issues: read + contents: write # allow Claude to push commits/branches when asked + pull-requests: write # allow Claude to comment on / update PRs + issues: write # allow Claude to comment on / update issues id-token: write actions: read # Required for Claude to read CI results on PRs steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 1 @@ -34,16 +44,25 @@ jobs: id: claude uses: anthropics/claude-code-action@v1 with: - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} # This is an optional setting that allows Claude to read CI results on PRs additional_permissions: | actions: read - # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it. - # prompt: 'Update the pull request description to include a summary of changes.' + # Sign the bot's commits so they show as "Verified". The action commits + # automatically — on a PR comment it pushes to that PR's branch; on an + # issue comment it opens a new claude/* branch + PR with the fix. + use_commit_signing: true - # Optional: Add claude_args to customize behavior and configuration - # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md - # or https://docs.claude.com/en/docs/claude-code/sdk#command-line for available options - # claude_args: '--model claude-opus-4-1-20250805 --allowed-tools Bash(gh pr:*)' + # No custom prompt: Claude performs the instructions in the @claude comment. + + # Deliberately NO Bash in the tool allowlist. @claude can be summoned on a + # fork PR (claude-code-review.yml even directs fork PRs here), and this job + # holds the OAuth secret + a write token. Any build/interpreter command + # (python -c, cmake/make custom targets, etc.) run against attacker- + # controlled PR content is arbitrary code + network execution, so no + # command allowlist can safely contain it. Claude still edits files and + # the action commits/opens the PR; the resulting commit is verified by the + # repo's CircleCI matrix. --max-turns gives room to investigate + fix. + claude_args: '--max-turns 30' diff --git a/.github/workflows/codeql-buildscript.sh b/.github/workflows/codeql-buildscript.sh deleted file mode 100644 index 272b55d22..000000000 --- a/.github/workflows/codeql-buildscript.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -FAMILY=stm32l4 -pip install click -python3 tools/get_deps.py $FAMILY -python3 tools/build.py -s make $FAMILY diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index dfcca6315..000000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,137 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - push: - branches: [ 'master' ] - paths: - - 'src/**' - - 'examples/**' - - 'lib/**' - - 'hw/**' - - '.github/workflows/codeql.yml' - pull_request: - branches: [ 'master' ] - paths: - - 'src/**' - - 'examples/**' - - 'lib/**' - - 'hw/**' - - '.github/workflows/codeql.yml' - schedule: - - cron: '0 0 * * *' - -jobs: - analyze: - name: Analyze - # Runner size impacts CodeQL analysis time. To learn more, please see: - # - https://gh.io/recommended-hardware-resources-for-running-codeql - # - https://gh.io/supported-runners-and-hardware-resources - # - https://gh.io/using-larger-runners - # Consider using larger runners for possible analysis time improvements. - runs-on: ubuntu-latest - timeout-minutes: 360 - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'c-cpp' ] - # CodeQL supports [ 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' ] - # Use only 'java-kotlin' to analyze code written in Java, Kotlin or both - # Use only 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both - # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup Toolchain - uses: ./.github/actions/setup_toolchain - with: - toolchain: 'arm-gcc' - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality - queries: security-and-quality - - - # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). - # If this step fails, then you should remove it and run the build manually (see below) - #- name: Autobuild - # uses: github/codeql-action/autobuild@v2 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - # If the Autobuild fails above, remove it and uncomment the following three lines. - # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - - - run: | - ./.github/workflows/codeql-buildscript.sh - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - with: - category: "/language:${{matrix.language}}" - upload: false - id: step1 - - # Filter out rules with low severity or high false positive rate - # Also filter out warnings in third-party code - - name: Filter out unwanted errors and warnings - uses: advanced-security/filter-sarif@v1 - with: - patterns: | - -**:cpp/path-injection - -**:cpp/world-writable-file-creation - -**:cpp/poorly-documented-function - -**:cpp/potentially-dangerous-function - -**:cpp/use-of-goto - -**:cpp/integer-multiplication-cast-to-long - -**:cpp/comparison-with-wider-type - -**:cpp/leap-year/* - -**:cpp/ambiguously-signed-bit-field - -**:cpp/suspicious-pointer-scaling - -**:cpp/suspicious-pointer-scaling-void - -**:cpp/unsigned-comparison-zero - -**/third*party/** - -**/3rd*party/** - -**/external/** - input: ${{ steps.step1.outputs.sarif-output }}/cpp.sarif - output: ${{ steps.step1.outputs.sarif-output }}/cpp.sarif - - - name: Upload SARIF - uses: github/codeql-action/upload-sarif@v3 - with: - sarif_file: ${{ steps.step1.outputs.sarif-output }} - category: "/language:${{matrix.language}}" - - - name: Upload CodeQL results as an artifact - uses: actions/upload-artifact@v4 - with: - name: codeql-results - path: ${{ steps.step1.outputs.sarif-output }} - retention-days: 5 diff --git a/.github/workflows/fail_on_error.py b/.github/workflows/fail_on_error.py deleted file mode 100755 index 29791742b..000000000 --- a/.github/workflows/fail_on_error.py +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env python3 - -import json -import sys - -# Return whether SARIF file contains error-level results -def codeql_sarif_contain_error(filename): - with open(filename, 'r') as f: - s = json.load(f) - - for run in s.get('runs', []): - rules_metadata = run['tool']['driver']['rules'] - if not rules_metadata: - rules_metadata = run['tool']['extensions'][0]['rules'] - - for res in run.get('results', []): - if 'ruleIndex' in res: - rule_index = res['ruleIndex'] - elif 'rule' in res and 'index' in res['rule']: - rule_index = res['rule']['index'] - else: - continue - try: - rule_level = rules_metadata[rule_index]['defaultConfiguration']['level'] - except IndexError as e: - print(e, rule_index, len(rules_metadata)) - else: - if rule_level == 'error': - return True - return False - -if __name__ == "__main__": - if codeql_sarif_contain_error(sys.argv[1]): - sys.exit(1) diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index c3cc59d0d..fe09413f1 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -4,23 +4,29 @@ on: issues: types: [opened] pull_request_target: - types: [opened] + types: [opened, synchronize, reopened] + discussion: + types: [created] jobs: label-priority: + # Author-based priority labels: only on issue/PR/discussion creation, not on PR updates. + if: github.event_name != 'pull_request_target' || github.event.action == 'opened' runs-on: ubuntu-latest permissions: issues: write pull-requests: write + discussions: write steps: - - name: Label New Issue or PR - uses: actions/github-script@v7 + - name: Label New Issue, PR or Discussion + uses: actions/github-script@v8 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - let label = ''; + let labels = []; let username = ''; let issueOrPrNumber = 0; + let discussionNodeId = ''; if (context.eventName === 'issues') { username = context.payload.issue.user.login; @@ -28,25 +34,85 @@ jobs: } else if (context.eventName === 'pull_request_target') { username = context.payload.pull_request.user.login; issueOrPrNumber = context.payload.pull_request.number; + } else if (context.eventName === 'discussion') { + username = context.payload.discussion.user.login; + discussionNodeId = context.payload.discussion.node_id; } - // Check if an Adafruit member - try { - const adafruitResponse = await github.rest.orgs.checkMembershipForUser({ - org: 'adafruit', - username: username - }); + // Maintainer is an Adafruit member; skip the Adafruit perks for their own + // issues/PRs and treat them as a plain contributor (Prio only). + const isOwner = username.toLowerCase() === 'hathach'; - if (adafruitResponse.status === 204) { - console.log('Adafruit Member'); - label = 'Prio Urgent'; + // Check if an Adafruit member: Adafruit + Sponsor + top priority + if (!isOwner) { + try { + const adafruitResponse = await github.rest.orgs.checkMembershipForUser({ + org: 'adafruit', + username: username + }); + + if (adafruitResponse.status === 204) { + console.log('Adafruit Member'); + labels = ['Adafruit 🌸', 'Sponsor 💖', 'Prio Top 🚨']; + } + } catch (error) { + console.log('Not an Adafruit member'); } - } catch (error) { - console.log('Not an Adafruit member'); } - // Check if a contributor - if (label == '') { + // Check if a public GitHub Sponsor of the repo owner. + // Word ($32) tier and up get triage priority; DWORD/QWORD ($128+) go to the top. + // Private sponsorships are not visible to GITHUB_TOKEN, so only public sponsors are detected. + if (labels.length === 0) { + try { + const result = await github.graphql(` + query($sponsorable: String!, $sponsor: String!) { + user(login: $sponsorable) { + isSponsoredBy(accountLogin: $sponsor) + sponsorshipsAsMaintainer(includePrivate: false, first: 100) { + nodes { + sponsorEntity { + ... on User { login } + ... on Organization { login } + } + tier { monthlyPriceInDollars } + } + } + } + }`, { sponsorable: context.repo.owner, sponsor: username }); + + const owner = result.user; + if (owner && owner.isSponsoredBy) { + let monthly = 0; + const nodes = (owner.sponsorshipsAsMaintainer && owner.sponsorshipsAsMaintainer.nodes) || []; + for (const node of nodes) { + const login = node.sponsorEntity && node.sponsorEntity.login; + if (login && login.toLowerCase() === username.toLowerCase()) { + monthly = (node.tier && node.tier.monthlyPriceInDollars) || 0; + break; + } + } + + if (monthly >= 128) { + console.log('Sponsor (DWORD/QWORD tier)'); + labels = ['Sponsor 💖', 'Prio Top 🚨']; + } else if (monthly >= 32) { + console.log('Sponsor (Word tier)'); + labels = ['Sponsor 💖', 'Prio 📌']; + } else { + console.log('Sponsor (below Word tier or tier not visible)'); + labels = ['Sponsor 💖']; + } + } else { + console.log('Not a public sponsor'); + } + } catch (error) { + console.log('Sponsor lookup failed: ' + error.message); + } + } + + // Check if a contributor: prioritized in triage queue + if (labels.length === 0) { try { const collaboratorResponse = await github.rest.repos.checkCollaborator({ owner: context.repo.owner, @@ -56,18 +122,61 @@ jobs: if (collaboratorResponse.status === 204) { console.log('Contributor'); - label = 'Prio Higher'; + labels = ['Prio 📌']; } } catch (error) { console.log('Not a contributor'); } } - if (label !== '') { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueOrPrNumber, - labels: [label] - }); + if (labels.length !== 0) { + if (context.eventName === 'discussion') { + // Discussions are not covered by the REST issues API; resolve the label + // names to node IDs and attach them with the GraphQL labelable mutation. + const labelIds = []; + for (const name of labels) { + const res = await github.graphql(` + query($owner: String!, $repo: String!, $name: String!) { + repository(owner: $owner, name: $repo) { + label(name: $name) { id } + } + }`, { owner: context.repo.owner, repo: context.repo.repo, name: name }); + if (res.repository.label) { + labelIds.push(res.repository.label.id); + } + } + if (labelIds.length !== 0) { + await github.graphql(` + mutation($labelableId: ID!, $labelIds: [ID!]!) { + addLabelsToLabelable(input: { labelableId: $labelableId, labelIds: $labelIds }) { + clientMutationId + } + }`, { labelableId: discussionNodeId, labelIds: labelIds }); + } + } else { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueOrPrNumber, + labels: labels + }); + } } + + # Path-based Port labels: attach "Port <ip>" when a PR touches the matching + # dcd/hcd driver under src/portable/. Mapping lives in .github/labeler.yml. + label-port: + if: github.event_name == 'pull_request_target' + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + issues: write # allow auto-creating a Port label that doesn't exist yet + steps: + - uses: actions/labeler@v5 + with: + configuration-path: .github/labeler.yml + # sync-labels so a Port label is removed once a PR no longer touches + # that driver (job reruns on synchronize). Only labels listed in + # labeler.yml are managed, so author-based Prio/Sponsor labels are untouched. + sync-labels: true diff --git a/.github/workflows/membrowse-comment.yml b/.github/workflows/membrowse-comment.yml new file mode 100644 index 000000000..952d8ba37 --- /dev/null +++ b/.github/workflows/membrowse-comment.yml @@ -0,0 +1,33 @@ +name: Membrowse Comment + +on: + workflow_run: + workflows: ["Build"] + types: + - completed + +jobs: + post-comment: + runs-on: ubuntu-latest + # Run the comment job even if some of the builds fail + if: > + github.event.workflow_run.event == 'pull_request' && + github.event.workflow_run.conclusion != 'cancelled' + permissions: + contents: read + actions: read + pull-requests: write + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Post Membrowse PR comment + if: ${{ env.MEMBROWSE_API_KEY != '' }} + uses: membrowse/membrowse-action/comment-action@v1 + with: + api_key: ${{ secrets.MEMBROWSE_API_KEY }} + commit: ${{ github.event.workflow_run.head_sha }} + comment_template: .github/membrowse_pr_message.j2 + env: + MEMBROWSE_API_KEY: ${{ secrets.MEMBROWSE_API_KEY }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/membrowse-onboard.yml b/.github/workflows/membrowse-onboard.yml new file mode 100644 index 000000000..4b9e54cff --- /dev/null +++ b/.github/workflows/membrowse-onboard.yml @@ -0,0 +1,62 @@ +name: Onboard to Membrowse + +on: + workflow_dispatch: + inputs: + num_commits: + description: 'Number of commits to process' + required: true + default: '10' + type: string + +jobs: + load-targets: + runs-on: ubuntu-22.04 + outputs: + targets: ${{ steps.load.outputs.targets }} + toolchains: ${{ steps.load.outputs.toolchains }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Load target matrix + id: load + run: | + echo "targets=$(jq -c '.targets' .github/membrowse-targets.json)" >> $GITHUB_OUTPUT + echo "toolchains=$(jq -c '.toolchains' .github/membrowse-targets.json)" >> $GITHUB_OUTPUT + + onboard: + needs: load-targets + runs-on: ubuntu-22.04 + strategy: + fail-fast: false + matrix: + include: ${{ fromJson(needs.load-targets.outputs.targets) }} + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + submodules: recursive + + - name: Install packages + run: | + ${{ fromJson(needs.load-targets.outputs.toolchains)[matrix.toolchain].setup_cmd }} && python3 tools/get_deps.py ${{ matrix.get_deps || matrix.port }} + + - name: Setup ccache + uses: hendrikmuhs/ccache-action@v1 + with: + key: ${{ matrix.port }}-${{ matrix.board }} + + - name: Run Membrowse Onboard Action + uses: membrowse/membrowse-action/onboard-action@v1 + with: + target_name: ${{ matrix.port }}-${{ matrix.board }}-${{ matrix.example }} + num_commits: ${{ github.event.inputs.num_commits }} + build_script: python3 tools/build.py -s cmake -b ${{ matrix.board }} + elf: cmake-build/cmake-build-${{ matrix.board }}/device/${{ matrix.example }}/${{ matrix.example }}.elf + ld: ${{ matrix.ld }} + linker_vars: ${{ matrix.linker_vars || '' }} + api_key: ${{ secrets.MEMBROWSE_API_KEY }} + api_url: ${{ vars.MEMBROWSE_API_URL }} diff --git a/.github/workflows/pr_comment.yml b/.github/workflows/pr_comment.yml new file mode 100644 index 000000000..4d50817b4 --- /dev/null +++ b/.github/workflows/pr_comment.yml @@ -0,0 +1,131 @@ +name: PR Comment + +on: + workflow_run: + workflows: ["Build"] + types: + - completed + +jobs: + # Resolve the PR number from trusted workflow_run metadata, NOT from build artifacts: a forked PR + # controls its own Build run and could plant any number, which the privileged jobs below would + # then post to. Same-repo PRs populate workflow_run.pull_requests; for forks it is empty, so look + # the PR up by the trusted head SHA. + pr_number: + if: github.event.workflow_run.event == 'pull_request' + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + outputs: + number: ${{ steps.resolve.outputs.number }} + steps: + - name: Resolve PR number + id: resolve + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} + HEAD_REPO: ${{ github.event.workflow_run.head_repository.full_name }} + PRS_JSON: ${{ toJSON(github.event.workflow_run.pull_requests) }} + run: | + # Every lookup is best-effort: on any miss the number stays empty and the comment jobs + # below simply skip (never a failed check). + # Same-repo PRs: workflow_run.pull_requests is populated. + num=$(printf '%s' "$PRS_JSON" | jq -r '.[0].number // empty') + # Fork PRs: pull_requests is empty. Find the open PR by its trusted head ref and confirm + # its head SHA matches the built commit. + if [ -z "$num" ] && [ -n "$HEAD_BRANCH" ] && [ -n "$HEAD_REPO" ]; then + num=$(gh api --method GET "repos/$REPO/pulls" \ + -f state=open -f head="${HEAD_REPO%%/*}:$HEAD_BRANCH" \ + --jq '[.[] | select(.head.sha == env.HEAD_SHA)][0].number // empty' 2>/dev/null || true) + fi + echo "number=$num" >> "$GITHUB_OUTPUT" + + metrics-comment: + needs: pr_number + if: > + github.event.workflow_run.conclusion == 'success' && + needs.pr_number.outputs.number != '' + runs-on: ubuntu-latest + permissions: + actions: read + pull-requests: write + steps: + - name: Download Artifacts + uses: actions/download-artifact@v5 + with: + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + name: metrics-comment + # Best-effort: docs-only PRs skip code-metrics, so the artifact may be absent. + continue-on-error: true + + - name: Post Code Metrics as PR Comment + if: hashFiles('metrics_compare.md') != '' + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: code-metrics + path: metrics_compare.md + number: ${{ needs.pr_number.outputs.number }} + + # --------------------------------------- + # Combine the rigs' HIL reports into one sticky PR comment (one table per rig). + # Runs here (workflow_run / base-repo context) rather than in build.yml so it also works on + # forked PRs, whose build-side GITHUB_TOKEN is read-only and cannot post comments. Posts even + # on build/HIL failure (when the report matters most); skips only on cancellation. + # --------------------------------------- + hil-comment: + needs: pr_number + if: > + github.event.workflow_run.conclusion != 'cancelled' && + needs.pr_number.outputs.number != '' + runs-on: ubuntu-latest + permissions: + actions: read + pull-requests: write + steps: + - name: Download HIL reports + uses: actions/download-artifact@v5 + with: + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + pattern: hil-report-* + path: hil-reports + continue-on-error: true + + - name: Combine rig reports (one table per rig) + id: combine + run: | + shopt -s nullglob + dirs=(hil-reports/hil-report-*) + if [ ${#dirs[@]} -eq 0 ]; then + echo "No HIL reports found" + exit 0 + fi + { + echo "## Hardware-in-the-loop (HIL) Test Report" + echo + for d in "${dirs[@]}"; do + [ -d "$d" ] || continue + echo "### ${d#hil-reports/hil-report-}" + echo + cat "$d/hil_report.md" 2>/dev/null || echo "_no report produced_" + echo + done + } > hil_combined.md + # Fork PRs can influence report content and this job posts in base-repo context, so + # neutralize @-mentions (insert a zero-width space) to prevent notification abuse. + zwsp=$(printf '\342\200\213') + sed -i -E "s/@([A-Za-z0-9_-])/@${zwsp}\1/g" hil_combined.md + cat hil_combined.md + echo "found=true" >> "$GITHUB_OUTPUT" + + - name: Post HIL report as sticky PR comment + if: steps.combine.outputs.found == 'true' + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: hil-report + path: hil_combined.md + number: ${{ needs.pr_number.outputs.number }} diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index ed0efd66e..b9bfaf9b6 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -20,7 +20,7 @@ jobs: ruby-version: '3.0' - name: Checkout TinyUSB - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Get Dependencies run: | diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml new file mode 100644 index 000000000..d440bf69e --- /dev/null +++ b/.github/workflows/static_analysis.yml @@ -0,0 +1,242 @@ +name: Static Analysis +on: + workflow_dispatch: + push: + branches: [ master ] + paths: + - 'src/**' + - 'examples/**' + - 'hw/bsp/**' + - '.github/workflows/static_analysis.yml' + pull_request: + branches: [ master ] + paths: + - 'src/**' + - 'examples/**' + - 'hw/bsp/**' + - '.github/workflows/static_analysis.yml' + +permissions: + actions: read + contents: read + security-events: write +# pull-requests: write +# checks: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + CodeQL: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + board: + - 'metro_m4_express' + steps: + - name: Checkout TinyUSB + uses: actions/checkout@v6 + + - name: Get Dependencies + uses: ./.github/actions/get_deps + with: + arg: -b${{ matrix.board }} + + - name: Setup Toolchain + uses: ./.github/actions/setup_toolchain + with: + toolchain: 'arm-gcc' + + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: 'c-cpp' + queries: security-and-quality + + - name: Build + run: | + mkdir -p build + cmake examples -B build -G Ninja -DBOARD=${{ matrix.board }} -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCMAKE_BUILD_TYPE=MinSizeRel + cmake --build build + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: CodeQL + upload: false + id: analyze + + - name: Filter SARIF report + uses: advanced-security/filter-sarif@v1 + with: + patterns: | + -hw/mcu/** + -lib/** + input: ${{ steps.analyze.outputs.sarif-output }}/cpp.sarif + output: ${{ steps.analyze.outputs.sarif-output }}/cpp.sarif + + - name: Upload SARIF + uses: github/codeql-action/upload-sarif@v4 + with: + sarif_file: ${{ steps.analyze.outputs.sarif-output }} + category: CodeQL + + - name: Upload artifact + uses: actions/upload-artifact@v7 + with: + name: codeql-${{ matrix.board }} + path: ${{ steps.analyze.outputs.sarif-output }} + + PVS-Studio: + # Only run on non-forked PR since secrets token is required + if: github.repository_owner == 'hathach' && github.event.pull_request.head.repo.fork == false + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + board: + - 'raspberry_pi_pico' + steps: + - name: Checkout TinyUSB + uses: actions/checkout@v6 + + - name: Get Dependencies + uses: ./.github/actions/get_deps + with: + arg: -b${{ matrix.board }} + + - name: Setup Toolchain + uses: ./.github/actions/setup_toolchain + with: + toolchain: 'arm-gcc' + + - name: Install Tools + run: | + wget -q -O - https://files.pvs-studio.com/etc/pubkey.txt | sudo apt-key add - + sudo wget -O /etc/apt/sources.list.d/viva64.list https://files.pvs-studio.com/etc/viva64.list + sudo apt update + sudo apt install pvs-studio + pvs-studio-analyzer credentials ${{ secrets.PVS_STUDIO_CREDENTIALS }} + pvs-studio-analyzer --version + + - name: Analyze + run: | + mkdir -p build + cmake examples -B build -G Ninja -DBOARD=${{ matrix.board }} -DCMAKE_BUILD_TYPE=MinSizeRel + cmake --build build + pvs-studio-analyzer analyze -f build/compile_commands.json -R .PVS-Studio/.pvsconfig -j4 --security-related-issues --misra-cpp-version 2008 --misra-c-version 2023 --use-old-parser -e lib/ -e hw/mcu/ -e */iar/cxarm/ -e pico-sdk/ + plog-converter -t sarif -o pvs-studio-${{ matrix.board }}.sarif PVS-Studio.log + + - name: Upload SARIF + uses: github/codeql-action/upload-sarif@v4 + with: + sarif_file: pvs-studio-${{ matrix.board }}.sarif + category: PVS-Studio + + - name: Upload artifact + uses: actions/upload-artifact@v7 + with: + name: pvs-studio-${{ matrix.board }} + path: pvs-studio-${{ matrix.board }}.sarif + + SonarQube: + # Only run on non-forked PR since secrets token is required + if: github.repository_owner == 'hathach' && github.event.pull_request.head.repo.fork == false + runs-on: ubuntu-latest + env: + BUILD_WRAPPER_OUT_DIR: build_wrapper_output_directory + strategy: + fail-fast: false + matrix: + board: + - 'stm32h743eval' + steps: + - name: Checkout TinyUSB + uses: actions/checkout@v6 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + + - name: Get Dependencies + uses: ./.github/actions/get_deps + with: + arg: -b${{ matrix.board }} + + - name: Setup Toolchain + uses: ./.github/actions/setup_toolchain + with: + toolchain: 'arm-gcc' + + - name: Install Build Wrapper + uses: SonarSource/sonarqube-scan-action/install-build-wrapper@v6 + + - name: Run Build Wrapper + run: | + cmake examples -B build -G Ninja -DBOARD=${{ matrix.board }} -DCMAKE_BUILD_TYPE=MinSizeRel + build-wrapper-linux-x86-64 --out-dir ${{ env.BUILD_WRAPPER_OUT_DIR }} cmake --build build/ + + - name: SonarQube Scan + uses: SonarSource/sonarqube-scan-action@v6 + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + with: + # Consult https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/scanners/sonarscanner/ for more information and options + args: > + --define sonar.cfamily.compile-commands=${{ env.BUILD_WRAPPER_OUT_DIR }}/compile_commands.json + + IAR-CStat: + # Only run on non-forked PR since secrets token is required + #if: github.repository_owner == 'hathach' && github.event.pull_request.head.repo.fork == false + if: false + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + board: + - 'b_g474e_dpow1' + steps: + - name: Checkout TinyUSB + uses: actions/checkout@v6 + + - name: Get Dependencies + uses: ./.github/actions/get_deps + with: + arg: -b${{ matrix.board }} + + - name: Setup Toolchain + uses: ./.github/actions/setup_toolchain + with: + toolchain: 'arm-iar' + + - name: Install CMake 4.2 + run: | + # IAR CSTAT requires CMake >= 4.1 + wget -q https://github.com/Kitware/CMake/releases/download/v4.2.0-rc1/cmake-4.2.0-rc1-linux-x86_64.tar.gz + tar -xzf cmake-4.2.0-rc1-linux-x86_64.tar.gz + echo "${{ github.workspace }}/cmake-4.2.0-rc1-linux-x86_64/bin" >> $GITHUB_PATH + + - name: Build and run IAR C-STAT Analysis + env: + IAR_LMS_BEARER_TOKEN: ${{ secrets.IAR_LMS_BEARER_TOKEN }} + run: | + # CMake run post build to generate C-STAT SARIF report + cmake --version + mkdir -p build + cmake examples/device/cdc_msc -B build -G Ninja -DBOARD=${{ matrix.board }} -DTOOLCHAIN=iar -DIAR_CSTAT=1 -DCMAKE_BUILD_TYPE=MinSizeRel + cmake --build build + # Merge sarif files for codeql upload + npm i -g @microsoft/sarif-multitool + npx @microsoft/sarif-multitool merge --merge-runs --output-file iar-cstat-${{ matrix.board }}.sarif build/cstat_sarif/*.sarif + + - name: Upload SARIF + uses: github/codeql-action/upload-sarif@v4 + with: + sarif_file: iar-cstat-${{ matrix.board }}.sarif + category: IAR-CStat + + - name: Upload artifact + uses: actions/upload-artifact@v7 + with: + name: iar-cstat-${{ matrix.board }} + path: iar-cstat-${{ matrix.board }}.sarif diff --git a/.github/workflows/trigger.yml b/.github/workflows/trigger.yml index cf40ac955..fd7c0b713 100644 --- a/.github/workflows/trigger.yml +++ b/.github/workflows/trigger.yml @@ -23,7 +23,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Push to tinyusb_src run: | |
