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/build_utils.py' - 'tools/ci_select.py' - 'tools/get_deps.py' - 'tools/rtt.py' - 'tools/membrowse_report.py' - '.github/ci-pinned-boards.json' - '.github/actions/**' - '.github/workflows/build.yml' - '.github/workflows/build_util.yml' - '.github/scripts/**' set-matrix: runs-on: ubuntu-latest outputs: json: ${{ steps.set-matrix-json.outputs.matrix }} hil_json: ${{ steps.set-matrix-json.outputs.hil_matrix }} example_map: ${{ steps.set-matrix-json.outputs.example_map }} # one pair per rig job: hil-tinyusb (tinyusb.json minus esptool boards), # hil-tinyusb-esp (esptool boards only), hil-tinyusb (hfp.json) hil_args_tinyusb: ${{ steps.hil-select.outputs.args_tinyusb }} hil_run_tinyusb: ${{ steps.hil-select.outputs.run_tinyusb }} hil_args_tinyusb_esp: ${{ steps.hil-select.outputs.args_tinyusb_esp }} hil_run_tinyusb_esp: ${{ steps.hil-select.outputs.run_tinyusb_esp }} hil_args_hfp: ${{ steps.hil-select.outputs.args_hfp }} hil_run_hfp: ${{ steps.hil-select.outputs.run_hfp }} steps: - name: Checkout TinyUSB uses: actions/checkout@v6 with: fetch-depth: 0 - name: CI selection (PR only) id: hil-select if: github.event_name == 'pull_request' env: BASE_REF: ${{ github.base_ref }} run: | # Best-effort by design: set-matrix gates cmake, hil-build and every rig job, # so a missing origin/, a shallow-clone hiccup or a selector traceback # must fall back to the FULL matrix (no --select, run=true, no args) instead # of failing the job. Same fail-open shape as pr_comment.yml's `|| true`. # # The selector's own unit suite gates it (stdlib-only, seconds): a selector # whose tests fail can still exit 0 with valid-but-WRONG JSON -- fail-open alone # never catches that class, and the pre-commit hil-test hook is a separate, # advisory workflow that nothing here can `needs:`. Test-failing selector => # full matrix, same as a crashing one. SELECT_JSON='' if ! python3 test/hil/test/test_ci_select.py; then echo "::warning::ci_select unit suite failed - falling back to the full HIL matrix" elif ! SELECT_JSON=$(python3 tools/ci_select.py --base "origin/$BASE_REF" test/hil/tinyusb.json test/hil/hfp.json); then echo "::warning::ci_select failed - falling back to the full HIL matrix" SELECT_JSON='' fi # The selection is handed on as a FILE in the workspace, never as a step # output/env var: it is ~KBs normally but a mass-sweep PR reaches hundreds of # KB, and an env var that big makes the consuming exec fail with E2BIG BEFORE # any fallback in it can run. Written here, ahead of its first reader. # No file (non-PR event, or any fallback) = full matrix. rm -f ci_select_out.json if [ -n "$SELECT_JSON" ]; then printf '%s' "$SELECT_JSON" > ci_select_out.json fi # One args/run pair per rig job, split by flasher: a job whose own subset is # empty skips explicitly instead of running a board filter that matches zero # boards ("No tests were run." exits 0 and would read as a green HIL run). OUT='' if [ -s ci_select_out.json ]; then OUT=$(python3 -c ' import json, re, sys s = json.load(open("ci_select_out.json")) # the same reading hil_ci_set_matrix.py applies: full false with no usable # boards map is an UNUSABLE selection, not "nothing selected". Both must agree # - one falling open to the whole roster while the other computes run=false # buys a full 37-leg build and still zero hardware coverage. if not s.get("full") and not isinstance(s.get("boards"), dict): sys.exit("selection has full false but no usable boards map") tin = s.get("args_flasher", {}).get("tinyusb.json", {}) legs = (("tinyusb", " ".join(a for f, a in sorted(tin.items()) if f != "esptool" and a)), ("tinyusb_esp", tin.get("esptool", "")), ("hfp", s.get("args", {}).get("hfp.json", ""))) for key, a in legs: # roster board names reach $GITHUB_OUTPUT as bare NAME=VALUE lines; a # newline in one would inject extra run_* lines and flip which rig jobs run. # ":" and "," are part of the normal shape - a partial filter is # `-bt :,` (ci_select._board_args) if not re.fullmatch(r"[-A-Za-z0-9_/ .=+:,]*", a): sys.exit("unexpected characters in the " + key + " board filter") print("args_" + key + "=" + a) print("run_" + key + "=" + ("true" if (s.get("full") or a) else "false")) ') || OUT='' if [ -z "$OUT" ]; then echo "::warning::ci_select output unusable - falling back to the full HIL matrix" # the same unusable selection must not stay behind for the build axis rm -f ci_select_out.json fi fi if [ -z "$OUT" ]; then OUT=$(for k in tinyusb tinyusb_esp hfp; do printf 'args_%s=\nrun_%s=true\n' "$k" "$k"; done) fi echo "$OUT" echo "$OUT" >> $GITHUB_OUTPUT - name: Generate matrix json id: set-matrix-json run: | # Build matrix, scoped by the PR selection when one exists. Best-effort: # ci_set_matrix falls back to the full matrix itself on unusable JSON, # and a missing file (non-PR event, selector fallback) means no flags. SELECT_FILE=ci_select_out.json [ -s "$SELECT_FILE" ] || SELECT_FILE='' BUILD_SELECT_FILE="$SELECT_FILE" MATRIX_JSON='' if [ -n "$SELECT_FILE" ]; then # ci_set_matrix falls open on a selection it cannot use with rc 0 - it prints # the full matrix and says UNSCOPED on stderr. The build extras below must # not stay scoped when it did, or a nominally full build compiles 1 of 44 # examples per family and membrowse uploads that partial run's sizes as if # it were the full build. Only the BUILD axis is dropped: build.families # being unusable says nothing about the boards map the HIL matrix reads. MATRIX_JSON=$(python .github/scripts/ci_set_matrix.py --select-file "$SELECT_FILE" 2>ci_set_matrix.err) || MATRIX_JSON='' cat ci_set_matrix.err >&2 if [ -z "$MATRIX_JSON" ] || grep -q 'ci_set_matrix: UNSCOPED' ci_set_matrix.err; then BUILD_SELECT_FILE='' fi fi [ -z "$MATRIX_JSON" ] && MATRIX_JSON=$(python .github/scripts/ci_set_matrix.py) # The per-family example map rides as a side channel: putting it inside # matrix entries would multiply GHA legs and break CircleCI's family parameter. EXAMPLE_MAP='{}' if [ -n "$BUILD_SELECT_FILE" ]; then EXAMPLE_MAP=$(jq -c '.build.family_examples // {}' "$BUILD_SELECT_FILE") || EXAMPLE_MAP='{}' fi echo "matrix=$MATRIX_JSON" echo "matrix=$MATRIX_JSON" >> $GITHUB_OUTPUT echo "example_map=$EXAMPLE_MAP" >> $GITHUB_OUTPUT # HIL matrix (merged from tinyusb + hifiphile configs), scoped on PRs. # Scoping is best-effort too: fall back to the unscoped (full) matrix. HIL_MATRIX_JSON='' if [ -n "$SELECT_FILE" ]; then HIL_MATRIX_JSON=$(python .github/scripts/hil_ci_set_matrix.py --select-file "$SELECT_FILE" test/hil/tinyusb.json test/hil/hfp.json) || HIL_MATRIX_JSON='' if [ -z "$HIL_MATRIX_JSON" ]; then echo "::warning::scoped HIL matrix failed - falling back to the full HIL matrix" fi fi if [ -z "$HIL_MATRIX_JSON" ]; then HIL_MATRIX_JSON=$(python .github/scripts/hil_ci_set_matrix.py test/hil/tinyusb.json test/hil/hfp.json) fi echo "hil_matrix=$HIL_MATRIX_JSON" echo "hil_matrix=$HIL_MATRIX_JSON" >> $GITHUB_OUTPUT # ------------------------------------------------------------------------------ # CMake build: membrowse's CI board(s) per family, falling back to one board (first # alphabetically) for families with no CI board. 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: '--ci-pinned-boards .github/ci-pinned-boards.json' example-map: ${{ needs.set-matrix.outputs.example_map }} upload-artifacts: false upload-membrowse: true code-changed: ${{ needs.check-paths.outputs.code_changed == 'true' }} secrets: inherit # --------------------------------------- # 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: # These names are the bucket keys of .github/scripts/hil_ci_set_matrix.py: every # non-esptool roster board must land in one of them (esptool boards go to # 'esp-idf', built by hil-build-esp below). hil_ci_set_matrix.py rejects a # board whose "toolchain" is not a bucket, so a new bucket must be added in # both places. # # INVARIANT the PR-scoped skip cascade rests on: hil_run_tinyusb / hil_run_hfp # true => hil-build has at least one non-empty leg. It holds because those # flags count only non-esptool boards and every such board builds here. It # would break if an esptool board were added to hfp.json, because # hil_args_hfp is NOT flasher-split: the hfp leg of hil-tinyusb would want to # run while hil-build (and therefore that leg) skipped. 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']) }} # build-args are explicit `-b ` flags (no bare family name), which # bypass --ci-pinned-boards family resolution entirely (tools/build.py main()) - this # is only here to satisfy --ci-pinned-boards-only's argparse requirement on --ci-pinned-boards build-options: '--ci-pinned-boards .github/ci-pinned-boards.json' upload-artifacts: true upload-membrowse: true secrets: inherit # espressif has no leg in the `cmake` job above (it would double-build the same # board on the slowest toolchain - see that job's comment), so unlike every other # CI board its --identical upload has nowhere to run on a no-code-change push: # hil-build-esp is gated on code_changed, and loosening that gate would let # hil-tinyusb-esp's hardware run start on a docs-only push too (its own SEL_RUN # gate is PR-only - see build.yml's "CI selection (PR only)" step - so on a push # event it defaults to "run everything", not "skip"). This job takes the opposite # gate instead, with no downstream dependents, so it can never reach the rig. # --identical needs neither a compiled elf nor a configured build dir # (tools/membrowse_report.py's --elf-missing path; tools/build.py's espressif # branch routes straight to it when there is no build dir), so plain python on # ubuntu-latest is enough - no ESP-IDF docker image, no idf.py, no compiler. hil-build-esp-identical: needs: [ check-paths ] if: needs.check-paths.outputs.code_changed != 'true' && github.repository_owner == 'hathach' runs-on: ubuntu-latest steps: - name: Checkout TinyUSB uses: actions/checkout@v6 with: fetch-depth: 0 - name: Install membrowse run: pip install membrowse - name: Membrowse identical upload (espressif, no code change) continue-on-error: true env: MEMBROWSE_API_KEY: ${{ secrets.MEMBROWSE_API_KEY }} run: | # one tools/build.py call per board, not one call for all of them: the # default shell here is `bash -eo pipefail`, and a later board must still # get its upload attempt even if an earlier one fails python3 .github/scripts/hil_ci_set_matrix.py test/hil/tinyusb.json | python3 -c 'import json, sys; [print(a) for a in json.load(sys.stdin)["esp-idf"]]' | while IFS= read -r arg; do # same variant guard as build_util.yml's Membrowse Upload step: a # -DMA variant leg configures with the same -DBOARD= as the # plain leg, and membrowse's target-name ({board}/{example}) is # derived from the board alone - uploading both would write two # --identical rows under the same target every no-code-change push case " $arg " in *' --build-name '*|*' -D'*|*' --cflag='*) echo "skip membrowse upload: variant build ($arg)"; continue ;; esac python3 tools/build.py -s cmake --target examples-membrowse-upload -j 1 $arg || true done # --------------------------------------- # Hardware in the loop (HIL) # self-hosted on local VM, for attached hardware checkout HIL_JSON # --------------------------------------- hil-tinyusb: needs: [ hil-build, set-matrix ] name: hil-tinyusb (${{ matrix.display }}) # Above hil_test.py's pool guard (HIL_POOL_TIMEOUT, 60 min) so the guard fires first # and still gets to write its report. The 30 min on top is what the job pays OUTSIDE # the guard clock: workspace cleanup, checkout, the multi-board artifact merge and # the D-state note before it; kill_worker_children, shutdown_pool's 30 s grace, the # report write and the upload after it. On a multi-stray convoy that tail alone is # minutes, and a ceiling below guard+tail cancels the job before hil_report.md exists # -- the inversion this branch removes. Both legs share the script and the guard. timeout-minutes: 90 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_POOL_TIMEOUT deliberately unset: hil_test.py's 60 min default is below every # ceiling here, so ceiling > guard holds by construction. Pin it to SHORTEN a run # only -- pinning it above a ceiling re-inverts the two. 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. env: # tinyusb.json minus the esptool boards (they run in hil-tinyusb-esp): each # job gates on its own flasher subset, never on a rig-wide flag SEL_ARGS_TINYUSB: ${{ needs.set-matrix.outputs.hil_args_tinyusb }} SEL_RUN_TINYUSB: ${{ needs.set-matrix.outputs.hil_run_tinyusb }} SEL_ARGS_HFP: ${{ needs.set-matrix.outputs.hil_args_hfp }} SEL_RUN_HFP: ${{ needs.set-matrix.outputs.hil_run_hfp }} run: | case "$HIL_JSON" in *tinyusb.json) SEL_ARGS="$SEL_ARGS_TINYUSB"; SEL_RUN="$SEL_RUN_TINYUSB" ;; *hfp.json) SEL_ARGS="$SEL_ARGS_HFP"; SEL_RUN="$SEL_RUN_HFP" ;; esac if [ "$SEL_RUN" = "false" ]; then echo "HIL skipped by PR selection (no affected boards on this rig)" # leave a marker so the combined PR comment says so instead of dropping the # section (and leaving a stale table from an earlier push in its place) mkdir -p "$HIL_REPORT_DIR" echo "_Skipped by PR selection: no affected boards on this rig._" > "$HIL_REPORT_DIR/hil_report.md" exit 0 fi # a re-run spec is already a subset of the selection (only the boards/tests # that failed); -b/-bt accumulate, so keeping SEL_ARGS here would re-run the # entire original selection instead of just what failed if [ -n "$RERUN_ARGS" ]; then SEL_ARGS=''; fi python3 test/hil/hil_test.py --retry 1 ${{ matrix.test_args }} $SEL_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, set-matrix ] name: hil-tinyusb (tinyusb-esp.json) runs-on: [ self-hosted, X64, hathach, hardware-in-the-loop ] # above hil_test.py's pool guard (60 min) with room for the pre-pool checkout # and the post-guard sweep + report upload, so its own guard still writes a report; # only a job wedged past that (unkillable D-state worker) hits this ceiling, which # must exist because the runner has one job slot and holds every queued job hostage timeout-minutes: 90 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 env: # esptool subset of tinyusb.json: this job must gate on its own boards, not # on the rig-wide flag (which would run a filter matching zero boards) SEL_ARGS: ${{ needs.set-matrix.outputs.hil_args_tinyusb_esp }} SEL_RUN: ${{ needs.set-matrix.outputs.hil_run_tinyusb_esp }} run: | if [ "$SEL_RUN" = "false" ]; then echo "HIL skipped by PR selection (no affected esptool boards)" # leave a marker so the combined PR comment says so instead of dropping the # section (and leaving a stale table from an earlier push in its place) mkdir -p "$HIL_REPORT_DIR" echo "_Skipped by PR selection: no affected esptool boards._" > "$HIL_REPORT_DIR/hil_report.md" exit 0 fi # a re-run spec is already a subset of the selection (only the boards/tests # that failed); -b/-bt accumulate, so keeping SEL_ARGS here would re-run the # entire original selection instead of just what failed if [ -n "$RERUN_ARGS" ]; then SEL_ARGS=''; fi python3 test/hil/hil_test.py --retry 1 $TEST_ARGS $SEL_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 ] # Unlike the hil-tinyusb jobs, this one BUILDS with IAR in the same job before running # hil_test.py -- hfp.json's 3 boards, 4 variant entries, "up to 30 minutes" (see the # comment above the selection step). The ceiling has to cover build + the 60 min pool # guard + overhead, or GitHub cancels before the guard can write its report -- the # inversion this branch removes. 30 + 60 = 90; the remaining 30 is the full-history # checkout, get_deps, the post-guard sweep and the report upload. timeout-minutes: 120 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 with: # full history: the "HIL selection" step below needs # merge-base(HEAD, origin/) for PR-scoped selection fetch-depth: 0 # Computed BEFORE the build: the IAR build is four boards and up to 30 minutes on # a runner that hil-tinyusb (hfp.json) also needs, so an unaffected PR must release # it immediately instead of building everything and then skipping. The selection # also narrows what gets built. # This job has no needs: on set-matrix (it must run even if that unrelated job # fails), so it computes its own selection instead of reading set-matrix's outputs. - name: HIL selection (PR only) if: github.event_name == 'pull_request' env: BASE_REF: ${{ github.base_ref }} run: | # Best-effort: this job is deliberately decoupled from set-matrix so unrelated # failures cannot kill hfp coverage - a selector failure here must likewise # fall back to the full hfp matrix (no ci_select.json, no SEL_* vars), never # fail the job. if ! python3 test/hil/test/test_ci_select.py; then echo "::warning::ci_select unit suite failed - running the full hfp matrix" rm -f ci_select.json exit 0 fi if ! python3 tools/ci_select.py --base "origin/$BASE_REF" test/hil/hfp.json > ci_select.json; then echo "::warning::ci_select failed - running the full hfp matrix" rm -f ci_select.json exit 0 fi # ci_select.json is passed to hil_ci_set_matrix.py --select below to scope the # build; it already honours full=true by ignoring the board list. # The hil_test.py args go to a file, never to $GITHUB_ENV: they are derived # from roster board names, which a PR can edit. Only SEL_RUN (a literal # true/false computed here, needed by the step-level `if:`) goes to the env. if ! SEL_RUN=$(python3 -c ' import json s = json.load(open("ci_select.json")) a = s["args"]["hfp.json"] open("hil_sel_args.txt", "w").write(a) print("true" if (s["full"] or a) else "false") '); then echo "::warning::ci_select output unusable - running the full hfp matrix" rm -f ci_select.json hil_sel_args.txt exit 0 fi echo "SEL_RUN=$SEL_RUN" echo "SEL_RUN=$SEL_RUN" >> $GITHUB_ENV - name: Get build boards if: env.SEL_RUN != 'false' run: | # --select-file, never --select "$(cat ...)": a whole selection as one argv # can exceed MAX_ARG_STRLEN on a big diff, and this job's design is to fall # back to the full hfp matrix on any selector trouble, not to fail the step. MATRIX_JSON='' if [ -f ci_select.json ]; then MATRIX_JSON=$(python .github/scripts/hil_ci_set_matrix.py --select-file ci_select.json test/hil/hfp.json) || MATRIX_JSON='' if [ -z "$MATRIX_JSON" ]; then echo "::warning::scoped hfp matrix failed - building the full hfp matrix" fi fi if [ -z "$MATRIX_JSON" ]; then MATRIX_JSON=$(python .github/scripts/hil_ci_set_matrix.py test/hil/hfp.json) fi # Each variant carries its own --build-name/--cflag, which are global to a # single build.py invocation — so keep one matrix entry per line and build # them one at a time (joining would leak a variant's flags onto every board). echo "$MATRIX_JSON" | jq -r '.["arm-gcc"][]' > hil_build_entries.txt cat hil_build_entries.txt BUILD_ARGS=$(echo "$MATRIX_JSON" | jq -r '.["arm-gcc"] | join(" ")') # board and example names are roster data a PR can edit, and jq -r un-escapes # them: a newline here writes extra NAME=VALUE lines into GITHUB_ENV for every # later step of a job that holds the IAR token. Refuse rather than guess. case "$BUILD_ARGS" in *[!-A-Za-z0-9_/\ .=+]*) echo "::error::unexpected characters in the hfp build args"; exit 1 ;; esac echo "BUILD_ARGS=$BUILD_ARGS" echo "BUILD_ARGS=$BUILD_ARGS" >> $GITHUB_ENV - name: Get Dependencies if: env.SEL_RUN != 'false' run: python3 tools/get_deps.py $BUILD_ARGS - name: Build if: env.SEL_RUN != 'false' # Bounded SEPARATELY from the job. This is the only HIL job that builds inline # (hil-tinyusb downloads artifacts), and the job ceiling went 30 -> 120 to give the # HIL step room -- which would hand a stalled IAR build the whole two hours on the # shared self-hosted runner, never reaching hil_test.py or the report upload. That # is the stranded-runner-with-no-report failure this branch exists to prevent. # Typical full build here is a few minutes; 30 leaves generous headroom. timeout-minutes: 30 run: | readarray -t ENTRIES < hil_build_entries.txt 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: | if [ "$SEL_RUN" = "false" ]; then echo "HIL skipped by PR selection (no affected boards on this rig)" # leave a marker so the combined PR comment says so instead of dropping # the section (and leaving a stale table from an earlier push) echo "_Skipped by PR selection: no affected boards on this rig._" > hil_report.md exit 0 fi # empty/absent on a non-PR event or a selector fallback -> full hfp matrix SEL_ARGS=$(cat hil_sel_args.txt 2>/dev/null || true) # --retry 1, like the other two HIL legs. The pool guard is a FLAT 3600s and # does NOT scale with max_retry, so argparse's default of 3 would multiply the # serialized usbtest tail (hfp.json runs four batteries) by three against an # unchanged guard -- on a runner with a single job slot that queues every other # job behind it. python3 test/hil/hil_test.py --retry 1 $SEL_ARGS 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