From 95d11a8a7a779e51643a578fe30c70466eb85cef Mon Sep 17 00:00:00 2001 From: hathach Date: Mon, 1 Jun 2026 23:51:24 +0700 Subject: docs(hil): support running HIL locally on ci.lan Update the hil skill so config selection is per-host: run `hostname` first, then htpc uses local.json and ci uses tinyusb.json. ci can now run HIL on its own large board pool locally instead of only via SSH from htpc. Remote (SSH) mode is htpc-only since ci cannot reach htpc. Also compact the skill for brevity. --- .claude/skills/hil/SKILL.md | 58 +++++++++++++++++---------------------------- 1 file changed, 22 insertions(+), 36 deletions(-) diff --git a/.claude/skills/hil/SKILL.md b/.claude/skills/hil/SKILL.md index 1f3d7d072..c705c149c 100644 --- a/.claude/skills/hil/SKILL.md +++ b/.claude/skills/hil/SKILL.md @@ -1,66 +1,52 @@ --- name: hil -description: Use when running TinyUSB Hardware-in-the-Loop (HIL) tests on physical boards, debugging HIL failures, or copying firmware to the ci.lan test rig. Covers local execution and remote execution over SSH, config selection, and debugging tips. +description: Use when running TinyUSB Hardware-in-the-Loop (HIL) tests on physical boards, debugging HIL failures, or copying firmware to the ci.lan test rig. Covers per-host config selection (htpc uses local.json, ci uses tinyusb.json), local execution on either htpc or ci, remote execution over SSH from htpc, and debugging tips. --- # Hardware-in-the-Loop (HIL) Testing -Run TinyUSB HIL tests against real boards. Two execution modes — **local** (boards attached to this machine) and **remote** (boards attached to `ci.lan`, reached over SSH). Default to **local** unless the user specifies `remote`. Do not auto-detect. +Run TinyUSB HIL tests on real boards. **Run `hostname` first** — it sets the default config and whether remote mode is possible. -## Prerequisites +| Host | Local boards | Remote (SSH → ci.lan)? | +|------|--------------|------------------------| +| `htpc` (dev PC) | `local.json` | yes (large pool, `tinyusb.json`) | +| `ci` (the rig) | `tinyusb.json` (large pool) | no — can't SSH to htpc, and boards are already local | -- Examples must already be built for the target board(s). See AGENTS.md "Build" → "All examples for a board", which produces `examples/cmake-build-/`. -- `-B examples` tells `hil_test.py` that `examples/` is the parent folder containing the per-board build outputs. +Default to **local**. Use **remote** only when on `htpc` and the user says `remote`/`ci.lan`. Never attempt remote on `ci`. -## Choosing arguments +## Prerequisites -Infer from the user's request: +Examples must be built for the target board(s) — see AGENTS.md "Build" → "All examples for a board" (produces `examples/cmake-build-/`). `-B examples` points `hil_test.py` at that parent folder. -- **Mode:** `local` (default) or `remote`. Only switch to `remote` if the user explicitly says so or names `ci.lan`. -- **Board:** if the user names a specific board, pass `-b BOARD_NAME`. Otherwise omit `-b` to run all boards in the config. -- **Pass-through flags:** `-v` (verbose), `-r N` (retry count), etc. — pass through unchanged. +## Arguments -Config file follows from mode: -- **Local** → `test/hil/local.json` (user-supplied; not tracked in repo — describes boards attached locally) -- **Remote** → `test/hil/tinyusb.json` (tracked; describes the `ci.lan` test rig) +- **Board:** `-b BOARD_NAME` for one board; omit to run all boards in the config. +- **Pass-through:** `-v`, `-r N`, etc. forwarded unchanged. -If `local.json` is missing, fall back to `tinyusb.json` only when explicitly told to; otherwise stop and ask the user to supply one. +If `local.json` is missing on `htpc`, ask the user to supply one (only fall back to `tinyusb.json` if told to). ## Local execution -Boards attached to this machine: +Pick `$CONFIG` from `hostname`: `local.json` on `htpc`, `tinyusb.json` on `ci`. ```bash -# Specific board: -python3 test/hil/hil_test.py -b BOARD_NAME -B examples test/hil/local.json $EXTRA_ARGS -# All boards in the config (no -b): -python3 test/hil/hil_test.py -B examples test/hil/local.json $EXTRA_ARGS +python3 test/hil/hil_test.py [-b BOARD_NAME] -B examples $CONFIG $EXTRA_ARGS ``` -## Remote execution (ci.lan) +## Remote execution (htpc → ci.lan only) -Use `test/hil/hil_ci.sh` — it handles dir setup, scp of test scripts, rsync of firmware artifacts (`.elf` / `.bin` / `.hex` only), and running `hil_test.py` on `ci.lan`: +`test/hil/hil_ci.sh` handles dir setup, scp of test scripts, rsync of firmware (`.elf`/`.bin`/`.hex`), and runs `hil_test.py` on `ci.lan` with `tinyusb.json`: ```bash -# Specific board: -bash test/hil/hil_ci.sh -b raspberry_pi_pico2 -# All boards in tinyusb.json: -bash test/hil/hil_ci.sh -# Pass-through extra args (any non -b flag is forwarded to hil_test.py): -bash test/hil/hil_ci.sh -b raspberry_pi_pico2 -t host/cdc_msc_hid -r 1 +bash test/hil/hil_ci.sh [-b BOARD_NAME] [extra hil_test.py args...] ``` -Overrides via env vars: `REMOTE=ci.lan`, `REMOTE_DIR=/tmp/tinyusb-hil`, `CONFIG=test/hil/tinyusb.json`. - -The script fails fast if the build dir or repo layout is missing. +Env overrides: `REMOTE`, `REMOTE_DIR`, `CONFIG`. Fails fast if the build dir/repo layout is missing. ## Timing -HIL runs take 2-5 minutes. Use a timeout of at least 20 minutes (600000 ms). NEVER cancel early. +Runs take 2-5 min. Use a timeout ≥ 20 min (600000 ms). NEVER cancel early. -## Reporting results +## Reporting -After the test completes: -- Show the test output to the user. -- Summarize pass/fail per board. -- On failure, suggest re-running with `-v` for verbose output. If `-v` isn't enough, temporarily add debug prints to `test/hil/hil_test.py` to pinpoint the issue. +Show the output, summarize pass/fail per board. On failure, retry with `-v`; if that's not enough, add temporary debug prints to `hil_test.py`. -- cgit v1.3.1 From 87f9cc01cfd3134594d196210d28ee590c7a5fc3 Mon Sep 17 00:00:00 2001 From: hathach Date: Mon, 1 Jun 2026 23:51:41 +0700 Subject: ci: re-enable Claude PR review and harden auth/permissions - claude-code-review.yml: re-enable (drop `if: false`); switch from pull_request_target to pull_request so fork PRs never receive the OAuth token (avoids prompt-injection token leak). Auto-review on open/synchronize/reopen/ready_for_review, skip drafts, sticky comment. - claude.yml: grant contents/pull-requests/issues write so @claude can reply and push fixes; @claude is the on-demand path for fork PRs. --- .github/workflows/claude-code-review.yml | 19 +++++++++++++++---- .github/workflows/claude.yml | 6 +++--- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 43144bb5e..2f055287c 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -1,18 +1,27 @@ name: Claude Code Review on: - pull_request_target: - types: [opened, synchronize, ready_for_review, reopened] + 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 from non-write-access + # contributors get NO token, so they are not auto-reviewed -> use @claude + # on those. Same-repo branches (yours or write-access contributors) get + # full auto-review safely. + types: [opened, synchronize, reopened, ready_for_review] jobs: claude-review: - if: false + # Skip drafts; review real PRs only + if: github.event.pull_request.draft == false runs-on: ubuntu-latest permissions: contents: read pull-requests: write issues: read id-token: write + actions: read # Required for Claude to read CI results on PRs steps: - name: Checkout repository @@ -28,5 +37,7 @@ jobs: plugin_marketplaces: 'https://github.com/anthropics/claude-code.git' plugins: 'code-review@claude-code-plugins' prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}' + # Reuse one comment instead of posting a new one each push + use_sticky_comment: true + claude_args: '--max-turns 20' # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md - # or https://code.claude.com/docs/en/cli-reference for available options diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 50f449949..660edfb7b 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -19,9 +19,9 @@ jobs: (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) 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: -- cgit v1.3.1 From 1ea04f7fe67bac927848e9dfd3ce2f607e7b93d7 Mon Sep 17 00:00:00 2001 From: hathach Date: Tue, 2 Jun 2026 00:16:54 +0700 Subject: ci: address codex/copilot review on claude workflows - claude.yml: gate @claude on author_association (OWNER/MEMBER/COLLABORATOR) so the write-scoped token and OAuth secret are never issued for an untrusted commenter on this public repo (defense-in-depth). - claude-code-review.yml: skip fork PRs in the job condition (head.repo.full_name == github.repository) since forks get no secrets and would only fail noisily; fix the misleading token comment; pass additional_permissions: actions: read so actions: read is effective. - hil SKILL.md: reword hostname guidance, use full test/hil/* paths, and show an explicit CONFIG= assignment so the local command is runnable. Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/skills/hil/SKILL.md | 14 ++++++++------ .github/workflows/claude-code-review.yml | 17 +++++++++++------ .github/workflows/claude.yml | 15 +++++++++++---- 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/.claude/skills/hil/SKILL.md b/.claude/skills/hil/SKILL.md index c705c149c..22588eba3 100644 --- a/.claude/skills/hil/SKILL.md +++ b/.claude/skills/hil/SKILL.md @@ -5,12 +5,12 @@ description: Use when running TinyUSB Hardware-in-the-Loop (HIL) tests on physic # Hardware-in-the-Loop (HIL) Testing -Run TinyUSB HIL tests on real boards. **Run `hostname` first** — it sets the default config and whether remote mode is possible. +Run TinyUSB HIL tests on real boards. **Run `hostname` first** — it tells you which host you are on, which determines the default config and whether remote mode is possible. -| Host | Local boards | Remote (SSH → ci.lan)? | +| Host | Local config | Remote (SSH → ci.lan)? | |------|--------------|------------------------| -| `htpc` (dev PC) | `local.json` | yes (large pool, `tinyusb.json`) | -| `ci` (the rig) | `tinyusb.json` (large pool) | no — can't SSH to htpc, and boards are already local | +| `htpc` (dev PC) | `test/hil/local.json` | yes (large pool, `test/hil/tinyusb.json`) | +| `ci` (the rig) | `test/hil/tinyusb.json` (large pool) | no — can't SSH to htpc, and boards are already local | Default to **local**. Use **remote** only when on `htpc` and the user says `remote`/`ci.lan`. Never attempt remote on `ci`. @@ -27,10 +27,12 @@ If `local.json` is missing on `htpc`, ask the user to supply one (only fall back ## Local execution -Pick `$CONFIG` from `hostname`: `local.json` on `htpc`, `tinyusb.json` on `ci`. +Set `CONFIG` from `hostname` first, then run: ```bash -python3 test/hil/hil_test.py [-b BOARD_NAME] -B examples $CONFIG $EXTRA_ARGS +CONFIG=test/hil/local.json # on htpc +# CONFIG=test/hil/tinyusb.json # on ci +python3 test/hil/hil_test.py [-b BOARD_NAME] -B examples "$CONFIG" $EXTRA_ARGS ``` ## Remote execution (htpc → ci.lan only) diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 2f055287c..6c8bbb03d 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -5,16 +5,18 @@ on: # opened/reopened/ready_for_review -> first auto review # synchronize -> auto re-review on new pushes # - # NOTE: pull_request (not _target) means fork PRs from non-write-access - # contributors get NO token, so they are not auto-reviewed -> use @claude - # on those. Same-repo branches (yours or write-access contributors) get - # full auto-review safely. + # 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; review real PRs only - if: github.event.pull_request.draft == false + # 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 @@ -34,6 +36,9 @@ jobs: uses: anthropics/claude-code-action@v1 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + # 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' prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}' diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 660edfb7b..dedecd349 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -12,11 +12,18 @@ on: 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: write # allow Claude to push commits/branches when asked -- cgit v1.3.1 From 044cd06f87117afa541d8cc743fcc9ef2bdc8089 Mon Sep 17 00:00:00 2001 From: hathach Date: Tue, 2 Jun 2026 00:24:05 +0700 Subject: ci: address second codex/copilot review round - claude.yml: drop the issues "assigned" trigger; its author_association gate keys on the issue author, not the assigner, so a maintainer assigning an outside contributor's issue would be wrongly skipped. - claude-code-review.yml: issues: read -> write so use_sticky_comment can create/update its PR comment via the issues API. - hil SKILL.md: make local/remote command blocks copy-pasteable (drop [-b BOARD_NAME] notation for concrete examples) and fix timeout (600000 ms is 10 min; use 1200000 ms for the stated 20 min). Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/skills/hil/SKILL.md | 22 ++++++++++++++++------ .github/workflows/claude-code-review.yml | 2 +- .github/workflows/claude.yml | 5 ++++- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/.claude/skills/hil/SKILL.md b/.claude/skills/hil/SKILL.md index 22588eba3..a7a916907 100644 --- a/.claude/skills/hil/SKILL.md +++ b/.claude/skills/hil/SKILL.md @@ -27,27 +27,37 @@ If `local.json` is missing on `htpc`, ask the user to supply one (only fall back ## Local execution -Set `CONFIG` from `hostname` first, then run: +Set `CONFIG` from `hostname` first (`test/hil/local.json` on htpc, `test/hil/tinyusb.json` on ci): ```bash -CONFIG=test/hil/local.json # on htpc -# CONFIG=test/hil/tinyusb.json # on ci -python3 test/hil/hil_test.py [-b BOARD_NAME] -B examples "$CONFIG" $EXTRA_ARGS +CONFIG=test/hil/local.json # on ci use: CONFIG=test/hil/tinyusb.json + +# All boards in the config: +python3 test/hil/hil_test.py -B examples "$CONFIG" + +# A single board (replace stm32f723disco): +python3 test/hil/hil_test.py -b stm32f723disco -B examples "$CONFIG" ``` +Append pass-through flags (`-v`, `-r 1`, …) to either command as needed. + ## Remote execution (htpc → ci.lan only) `test/hil/hil_ci.sh` handles dir setup, scp of test scripts, rsync of firmware (`.elf`/`.bin`/`.hex`), and runs `hil_test.py` on `ci.lan` with `tinyusb.json`: ```bash -bash test/hil/hil_ci.sh [-b BOARD_NAME] [extra hil_test.py args...] +# All boards: +bash test/hil/hil_ci.sh + +# A single board, with pass-through flags: +bash test/hil/hil_ci.sh -b raspberry_pi_pico2 -t host/cdc_msc_hid -r 1 ``` Env overrides: `REMOTE`, `REMOTE_DIR`, `CONFIG`. Fails fast if the build dir/repo layout is missing. ## Timing -Runs take 2-5 min. Use a timeout ≥ 20 min (600000 ms). NEVER cancel early. +Runs take 2-5 min. Use a timeout ≥ 20 min (1200000 ms). NEVER cancel early. ## Reporting diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 6c8bbb03d..4a0e4639b 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -21,7 +21,7 @@ jobs: permissions: contents: read pull-requests: write - issues: read + issues: write # use_sticky_comment posts/updates a PR comment via the issues API id-token: write actions: read # Required for Claude to read CI results on PRs diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index dedecd349..bf7a401e4 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -6,7 +6,10 @@ 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] -- cgit v1.3.1