diff options
Diffstat (limited to '.github')
| -rwxr-xr-x | .github/scripts/ci_set_matrix.py (renamed from .github/workflows/ci_set_matrix.py) | 1 | ||||
| -rw-r--r-- | .github/scripts/hil_ci_set_matrix.py | 92 | ||||
| -rw-r--r-- | .github/workflows/build.yml | 370 | ||||
| -rw-r--r-- | .github/workflows/build_util.yml | 17 | ||||
| -rw-r--r-- | .github/workflows/pr_comment.yml | 9 | ||||
| -rw-r--r-- | .github/workflows/pre-commit.yml | 5 |
6 files changed, 460 insertions, 34 deletions
diff --git a/.github/workflows/ci_set_matrix.py b/.github/scripts/ci_set_matrix.py index dc0d3871f..50ada5964 100755 --- a/.github/workflows/ci_set_matrix.py +++ b/.github/scripts/ci_set_matrix.py @@ -16,6 +16,7 @@ toolchain_list = [ # family: [supported toolchain] family_list = { + "apm32f0xx": ["arm-gcc"], "at32f402_405": ["arm-gcc"], "at32f403a_407": ["arm-gcc"], "at32f413": ["arm-gcc"], diff --git a/.github/scripts/hil_ci_set_matrix.py b/.github/scripts/hil_ci_set_matrix.py new file mode 100644 index 000000000..65f50788e --- /dev/null +++ b/.github/scripts/hil_ci_set_matrix.py @@ -0,0 +1,92 @@ +import argparse +import json +import os + + +def _resolve_config_path(config_file): + if os.path.exists(config_file): + return config_file + + # bare roster names resolve against the repo's test/hil (this script lives in + # .github/scripts); build.yml passes explicit paths, this is for hand-runs + repo_relative = os.path.join(os.path.dirname(__file__), '..', '..', 'test', 'hil', config_file) + if os.path.exists(repo_relative): + return repo_relative + + raise FileNotFoundError(f'Config file not found: {config_file}') + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('config_files', nargs='+', help='Configuration JSON file(s)') + parser.add_argument('--select', help='hil_select.py JSON; scopes boards when full=false') + args = parser.parse_args() + + selected = None + sel = json.loads(args.select) if args.select else None + if sel and not sel.get('full'): + selected = set(sel.get('boards', {})) + + # Toolchain buckets must match the toolchains instantiated by the hil-build + # job in .github/workflows/build.yml. Keep all keys present (even if empty) + # so `fromJSON(hil_json)[toolchain]` always resolves to a list. + matrix = { + 'arm-gcc': [], + 'riscv-gcc': [], + 'esp-idf': [] + } + + seen = {toolchain: set() for toolchain in matrix} + + def append_build_arg(toolchain, build_arg): + if build_arg not in seen[toolchain]: + seen[toolchain].add(build_arg) + matrix[toolchain].append(build_arg) + + for config_file in args.config_files: + with open(_resolve_config_path(config_file)) as f: + config = json.load(f) + + for board in config['boards']: + if selected is not None and board['name'] not in selected: + continue + name = board['name'] + flasher = board['flasher'] + # esptool boards must build under esp-idf; others default to arm-gcc + # but may opt into another bucket via an explicit "toolchain" field + # (e.g. RISC-V boards like ch32v20x need "riscv-gcc"). + if flasher['name'] == 'esptool': + toolchain = 'esp-idf' + else: + toolchain = board.get('toolchain', 'arm-gcc') + if toolchain not in matrix: + # a board in no bucket would never be built, and the bare KeyError + # below would only say so as a traceback from the set-matrix job + raise SystemExit( + f'{name}: toolchain {toolchain!r} is not a build bucket ' + f'({", ".join(matrix)}); add it here and to the hil-build / ' + f'hil-build-esp jobs in .github/workflows/build.yml') + + build_board = f'-b {name}' + if 'build' in board and 'args' in board['build']: + build_board += ' ' + ' '.join(f'-D{a}' for a in board['build']['args']) + + # Each variant builds into cmake-build-<variant.name> with its own cmake + # -D defines and raw CFLAGS. No 'variant' -> a single build named after + # the board. + variants = board.get('variant') or [{'name': name, 'flags': ''}] + for v in variants: + arg = build_board + if v['name'] != name: + arg += f' --build-name {v["name"]}' + for d in v.get('defines', []): + arg += f' -D{d}' + for tok in v.get('flags', '').split(): + arg += f' --cflag={tok}' + append_build_arg(toolchain, arg) + + print(json.dumps(matrix)) + + +if __name__ == '__main__': + main() diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c8c597e50..8f6014f48 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -41,27 +41,100 @@ jobs: - '.github/actions/**' - '.github/workflows/build.yml' - '.github/workflows/build_util.yml' - - '.github/workflows/ci_set_matrix.py' + - '.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 }} + # 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: HIL 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/<base>, 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_hil_select.py; then + echo "::warning::hil_select unit suite failed - falling back to the full HIL matrix" + elif ! SELECT_JSON=$(python3 test/hil/helper/hil_select.py --base "origin/$BASE_REF" test/hil/tinyusb.json test/hil/hfp.json); then + echo "::warning::hil_select failed - falling back to the full HIL matrix" + SELECT_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 [ -n "$SELECT_JSON" ]; then + OUT=$(SELECT_JSON="$SELECT_JSON" python3 -c ' + import json, os + s = json.loads(os.environ["SELECT_JSON"]) + 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: + print("args_" + key + "=" + a) + print("run_" + key + "=" + ("true" if (s.get("full") or a) else "false")) + ') || OUT='' + if [ -z "$OUT" ]; then + echo "::warning::hil_select output unusable - falling back to the full HIL matrix" + SELECT_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 "select=$SELECT_JSON"; echo "$OUT"; } >> $GITHUB_OUTPUT - name: Generate matrix json id: set-matrix-json + env: + SELECT: ${{ steps.hil-select.outputs.select }} run: | # build matrix - MATRIX_JSON=$(python .github/workflows/ci_set_matrix.py) + MATRIX_JSON=$(python .github/scripts/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) + # 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" ]; then + HIL_MATRIX_JSON=$(python .github/scripts/hil_ci_set_matrix.py --select "$SELECT" 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 @@ -271,23 +344,55 @@ jobs: 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' - - 'esp-idf' 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 + 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: @@ -295,26 +400,39 @@ jobs: - 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 (sibling of workspace; persists across run attempts) - run: echo "HIL_REPORT_DIR=$(dirname "$GITHUB_WORKSPACE")/hil-report" >> "$GITHUB_ENV" + - 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 Skip Boards from previous run + - name: Get re-run spec from previous attempt 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 + # 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: | @@ -333,15 +451,33 @@ jobs: 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: | - 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) + 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' @@ -353,6 +489,92 @@ jobs: 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 @@ -364,7 +586,13 @@ jobs: 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 + # 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' @@ -381,23 +609,87 @@ jobs: - name: Checkout TinyUSB uses: actions/checkout@v6 + with: + # full history: the "HIL selection" step below needs + # merge-base(HEAD, origin/<base_ref>) 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 hil_select.json, no SEL_* vars), never + # fail the job. + if ! python3 test/hil/test/test_hil_select.py; then + echo "::warning::hil_select unit suite failed - running the full hfp matrix" + rm -f hil_select.json + exit 0 + fi + if ! python3 test/hil/helper/hil_select.py --base "origin/$BASE_REF" test/hil/hfp.json > hil_select.json; then + echo "::warning::hil_select failed - running the full hfp matrix" + rm -f hil_select.json + exit 0 + fi + # hil_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("hil_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::hil_select output unusable - running the full hfp matrix" + rm -f hil_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: | - MATRIX_JSON=$(python test/hil/hil_ci_set_matrix.py test/hil/hfp.json) - BUILD_ARGS=$(echo $MATRIX_JSON | jq -r '.["arm-gcc"] | join(" ")') + if [ -f hil_select.json ]; then + MATRIX_JSON=$(python .github/scripts/hil_ci_set_matrix.py --select "$(cat hil_select.json)" test/hil/hfp.json) + else + 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(" ")') 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: | - # 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"][]') + 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 @@ -405,7 +697,21 @@ jobs: - name: Test on actual hardware (hardware in the loop) run: | - python3 test/hil/hil_test.py hfp.json + 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' diff --git a/.github/workflows/build_util.yml b/.github/workflows/build_util.yml index 2532caebe..02f16488a 100644 --- a/.github/workflows/build_util.yml +++ b/.github/workflows/build_util.yml @@ -39,6 +39,21 @@ on: jobs: family: + # PR-scoped HIL selection can produce an empty build-args list for a toolchain + # (e.g. a dwc2-only change with no riscv boards affected); an empty matrix + # vector fails the job outright ("Matrix vector 'arg' does not contain any + # values"), so skip cleanly instead. + # + # Why that is safe for callers: GitHub SKIPS the dependents of a skipped + # `needs:` job, so this only works because the caller (hil-build) is itself a + # *matrix* job - a matrix with one skipped leg and one successful leg + # aggregates to success, and its dependents run. + # + # NOT covered: if every leg is empty the whole caller job skips, and so does + # everything that needs it. That is fine only because an all-empty selection + # means no board was selected for those rigs, so the rig jobs would have had + # nothing to run anyway (see the invariant on hil-build in build.yml). + if: inputs.build-args != '[]' runs-on: ${{ inputs.os }} strategy: fail-fast: false @@ -99,7 +114,7 @@ jobs: if: inputs.upload-artifacts == true && inputs.code-changed == true uses: actions/upload-artifact@v7 with: - name: binaries-${{ matrix.arg }} + name: binaries-${{ inputs.toolchain }}-${{ matrix.arg }} path: | cmake-build/cmake-build-*/*/*/*.elf cmake-build/cmake-build-*/*/*/*.bin diff --git a/.github/workflows/pr_comment.yml b/.github/workflows/pr_comment.yml index 4d50817b4..868e56405 100644 --- a/.github/workflows/pr_comment.yml +++ b/.github/workflows/pr_comment.yml @@ -101,7 +101,16 @@ jobs: shopt -s nullglob dirs=(hil-reports/hil-report-*) if [ ${#dirs[@]} -eq 0 ]; then + # No rig produced a report: PR selection matched no board anywhere, or the + # HIL jobs did not run at all. Post it rather than exiting, so a table from + # an earlier push is replaced instead of being left to look current. echo "No HIL reports found" + { + echo "## Hardware-in-the-loop (HIL) Test Report" + echo + echo "_No HIL run for this push (no affected boards, or hardware testing did not run)._" + } > hil_combined.md + echo "found=true" >> "$GITHUB_OUTPUT" exit 0 fi { diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index b9bfaf9b6..09f912bd3 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -24,10 +24,13 @@ jobs: - name: Get Dependencies run: | - gem install ceedling + # pinned: 1.1.0 breaks mock preprocessing on this project ("Failed to read + # _build/test/preprocess/.../raw/*.h for comment stripping"); revisit on next release + gem install ceedling -v 1.0.1 #cd test/unit-test #ceedling test:all + # runs --all-files, so the hil-test hook fires here regardless of its `files:` scope - name: Run pre-commit uses: pre-commit/[email protected] |
