name: Build on: workflow_dispatch: push: branches: [master] pull_request: release: types: [ published ] 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: json: ${{ steps.set-matrix-json.outputs.matrix }} hil_json: ${{ steps.set-matrix-json.outputs.hil_matrix }} steps: - name: Checkout TinyUSB uses: actions/checkout@v6 - name: Generate matrix json id: set-matrix-json run: | # build matrix MATRIX_JSON=$(python .github/workflows/ci_set_matrix.py) echo "matrix=$MATRIX_JSON" echo "matrix=$MATRIX_JSON" >> $GITHUB_OUTPUT # 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 # ------------------------------------------------------------------------------ # 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: needs: [ check-paths, set-matrix ] uses: ./.github/workflows/build_util.yml strategy: fail-fast: false matrix: toolchain: - 'aarch64-gcc' #- 'arm-clang' - 'arm-gcc' #- '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]) }} build-options: '--one-first' upload-metrics: true upload-artifacts: false upload-membrowse: true code-changed: ${{ needs.check-paths.outputs.code_changed == 'true' }} secrets: inherit 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 # --------------------------------------- # 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. # --------------------------------------- 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/CMake on Windows/MacOS # --------------------------------------- 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 ] build-system: [ 'make', 'cmake' ] with: os: ${{ matrix.os }} build-system: ${{ matrix.build-system }} toolchain: 'arm-gcc-${{ matrix.os }}' build-args: '["stm32h7rs"]' build-options: '--one-random' # --------------------------------------- # Zephyr # --------------------------------------- zephyr: 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@v6 - name: Setup Zephyr project uses: zephyrproject-rtos/action-zephyr-setup@v1 with: app-path: examples toolchains: arm-zephyr-eabi - name: Build run: | 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-iar only run on non-forked PR # --------------------------------------- hil-build: 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' with: build-system: 'cmake' toolchain: ${{ matrix.toolchain }} build-args: ${{ toJSON(fromJSON(needs.set-matrix.outputs.hil_json)[matrix.toolchain]) }} upload-artifacts: true # esp-idf builds are by far the slowest; keep them out of hil-build so the main # hil-tinyusb run starts as soon as the fast toolchains finish (esp boards get # their own hil-tinyusb-esp run gated only on this job) hil-build-esp: needs: [ check-paths, set-matrix ] if: needs.check-paths.outputs.code_changed == 'true' && github.repository_owner == 'hathach' uses: ./.github/workflows/build_util.yml with: build-system: 'cmake' toolchain: 'esp-idf' build-args: ${{ toJSON(fromJSON(needs.set-matrix.outputs.hil_json)['esp-idf']) }} upload-artifacts: true # --------------------------------------- # Hardware in the loop (HIL) # self-hosted on local VM, for attached hardware checkout HIL_JSON # --------------------------------------- hil-tinyusb: needs: hil-build 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 # esptool-flashed (espressif) boards run in hil-tinyusb-esp, # gated on the slow esp-idf build test_args: '--exclude-flasher esptool' - display: hfp.json runner: [ self-hosted, Linux, X64, hifiphile ] hil_json: test/hil/hfp.json test_args: '' runs-on: ${{ matrix.runner }} env: HIL_JSON: ${{ matrix.hil_json }} steps: - name: Set HIL report dir (per run+job; persists across run attempts) run: | # one report dir per (run id, job): re-run attempts find their own report/spec, # and interleaved runs of other PRs/jobs on the same runner cannot clobber them BASE="$(dirname "$GITHUB_WORKSPACE")/hil-report" # prune per-run dirs older than 2 weeks find "$BASE" -mindepth 1 -maxdepth 1 -type d -mtime +14 -exec rm -rf {} + 2>/dev/null || true echo "HIL_REPORT_DIR=$BASE/${GITHUB_RUN_ID}-$(basename "${{ matrix.display }}" .json)" >> "$GITHUB_ENV" - name: Get re-run spec from previous attempt if: github.run_attempt != '1' run: | # the report dir is keyed by run id, so a spec here can only have been # written by an earlier attempt of THIS run SPEC="$HIL_REPORT_DIR/$(basename "${{ env.HIL_JSON }}").failed" RERUN_ARGS="" [ -f "$SPEC" ] && RERUN_ARGS=$(cat "$SPEC") echo "RERUN_ARGS=$RERUN_ARGS" echo "RERUN_ARGS=$RERUN_ARGS" >> $GITHUB_ENV - name: Clean workspace run: | echo "Cleaning up for the first run" rm -rf "${{ github.workspace }}" mkdir -p "${{ github.workspace }}" - name: Checkout TinyUSB uses: actions/checkout@v6 - name: Download Artifacts uses: actions/download-artifact@v5 with: pattern: binaries-* path: cmake-build merge-multiple: true - name: Test on actual hardware # Single attempt per test (--retry 1), no in-run second pass: a broken fixture # fails fast instead of holding the runner (and other PRs' HIL jobs) for hours. run: python3 test/hil/hil_test.py --retry 1 ${{ matrix.test_args }} ${{ env.HIL_JSON }} $RERUN_ARGS - 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) - espressif boards only # Same rig as hil-tinyusb (tinyusb.json) but gated only on the slow esp-idf build, # so the main run does not wait for it. Per-board flocks arbitrate the shared rig; # the runner has a single job slot, so the two HIL jobs never overlap - adding a # second slot would double the per-controller flash/usbtest budgets. # --------------------------------------- hil-tinyusb-esp: needs: hil-build-esp name: hil-tinyusb (tinyusb-esp.json) runs-on: [ self-hosted, X64, hathach, hardware-in-the-loop ] env: HIL_JSON: test/hil/tinyusb.json TEST_ARGS: '--flasher esptool' steps: - name: Set HIL report dir (per run+job; persists across run attempts) run: | # one report dir per (run id, job): re-run attempts find their own report/spec, # and interleaved runs of other PRs/jobs on the same runner cannot clobber them BASE="$(dirname "$GITHUB_WORKSPACE")/hil-report" # prune per-run dirs older than 2 weeks find "$BASE" -mindepth 1 -maxdepth 1 -type d -mtime +14 -exec rm -rf {} + 2>/dev/null || true echo "HIL_REPORT_DIR=$BASE/${GITHUB_RUN_ID}-tinyusb-esp" >> "$GITHUB_ENV" - name: Get re-run spec from previous attempt if: github.run_attempt != '1' run: | # the report dir is keyed by run id, so a spec here can only have been # written by an earlier attempt of THIS run SPEC="$HIL_REPORT_DIR/$(basename "${{ env.HIL_JSON }}").failed" RERUN_ARGS="" [ -f "$SPEC" ] && RERUN_ARGS=$(cat "$SPEC") echo "RERUN_ARGS=$RERUN_ARGS" echo "RERUN_ARGS=$RERUN_ARGS" >> $GITHUB_ENV - name: Clean workspace run: | echo "Cleaning up for the first run" rm -rf "${{ github.workspace }}" mkdir -p "${{ github.workspace }}" - name: Checkout TinyUSB uses: actions/checkout@v6 - name: Download Artifacts uses: actions/download-artifact@v5 with: pattern: binaries-esp-idf-* path: cmake-build merge-multiple: true - name: Test on actual hardware run: python3 test/hil/hil_test.py --retry 1 $TEST_ARGS ${{ env.HIL_JSON }} $RERUN_ARGS - name: Upload HIL report if: always() && github.event_name == 'pull_request' uses: actions/upload-artifact@v7 with: name: hil-report-tinyusb-esp.json 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-iar: needs: [ check-paths ] if: | needs.check-paths.outputs.code_changed == 'true' && github.repository_owner == 'hathach' && !(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 }}" mkdir -p "${{ github.workspace }}" - name: Toolchain version run: | iccarm --version - name: Checkout TinyUSB uses: actions/checkout@v6 - name: Get build boards run: | MATRIX_JSON=$(python test/hil/hil_ci_set_matrix.py test/hil/hfp.json) BUILD_ARGS=$(echo $MATRIX_JSON | jq -r '.["arm-gcc"] | join(" ")') echo "BUILD_ARGS=$BUILD_ARGS" echo "BUILD_ARGS=$BUILD_ARGS" >> $GITHUB_ENV - name: Get Dependencies run: python3 tools/get_deps.py $BUILD_ARGS - name: Build 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 - 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