summaryrefslogtreecommitdiff
path: root/.github
diff options
context:
space:
mode:
authorhathach <[email protected]>2026-06-11 17:20:15 +0700
committerhathach <[email protected]>2026-06-11 17:20:15 +0700
commit5145b67f7946763ca04954f5494bfa88eec2acad (patch)
tree9130540204c2404d532613adab01e88f4e918daa /.github
parentf52d9c7b27dc0234fb4738762a8a6da01817e2bf (diff)
parent0244a4f12e019406a4b73c75cd96f8efa096bbdc (diff)
Merge remote-tracking branch 'origin/master' into stm32c5
# Conflicts: # README.rst
Diffstat (limited to '.github')
-rw-r--r--.github/FUNDING.yml1
-rw-r--r--.github/labeler.yml77
-rw-r--r--.github/workflows/build.yml103
-rw-r--r--.github/workflows/build_util.yml2
-rw-r--r--.github/workflows/claude-code-review.yml37
-rw-r--r--.github/workflows/claude.yml47
-rw-r--r--.github/workflows/labeler.yml159
7 files changed, 379 insertions, 47 deletions
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 000000000..e9930a25a
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1 @@
+github: hathach
diff --git a/.github/labeler.yml b/.github/labeler.yml
new file mode 100644
index 000000000..6c7aa7e7d
--- /dev/null
+++ b/.github/labeler.yml
@@ -0,0 +1,77 @@
+# Path-based auto-labeling for USB IP / port drivers.
+# Maps changed dcd/hcd files under src/portable/ to their "Port <ip>" label.
+# Consumed by actions/labeler (see .github/workflows/labeler.yml -> label-port job).
+
+"Port DWC2":
+ - changed-files:
+ - any-glob-to-any-file: 'src/portable/synopsys/dwc2/**'
+
+"Port EHCI":
+ - changed-files:
+ - any-glob-to-any-file: 'src/portable/ehci/**'
+
+"Port OHCI":
+ - changed-files:
+ - any-glob-to-any-file: 'src/portable/ohci/**'
+
+"Port FSDev":
+ - changed-files:
+ - any-glob-to-any-file: 'src/portable/st/stm32_fsdev/**'
+
+"Port ChipIdea":
+ - changed-files:
+ - any-glob-to-any-file: 'src/portable/chipidea/**'
+
+"Port NXP IP3511":
+ - changed-files:
+ - any-glob-to-any-file: 'src/portable/nxp/lpc_ip3511/**'
+
+"Port NXP IP3516":
+ - changed-files:
+ - any-glob-to-any-file: 'src/portable/nxp/lpc_ip3516/**'
+
+"Port MUSB":
+ - changed-files:
+ - any-glob-to-any-file:
+ - 'src/portable/mentor/musb/**'
+ - 'src/portable/sunxi/**'
+
+"Port RUSB2":
+ - changed-files:
+ - any-glob-to-any-file: 'src/portable/renesas/rusb2/**'
+
+"Port WCH USBFS":
+ - changed-files:
+ - any-glob-to-any-file: 'src/portable/wch/*usbfs*'
+
+"Port WCH USBHS":
+ - changed-files:
+ - any-glob-to-any-file: 'src/portable/wch/*usbhs*'
+
+"Port MAX3421":
+ - changed-files:
+ - any-glob-to-any-file: 'src/portable/analog/max3421/**'
+
+"Port SAMD":
+ - changed-files:
+ - any-glob-to-any-file: 'src/portable/microchip/samd/**'
+
+"Port SAMG":
+ - changed-files:
+ - any-glob-to-any-file: 'src/portable/microchip/samg/**'
+
+"Port nRF":
+ - changed-files:
+ - any-glob-to-any-file: 'src/portable/nordic/nrf5x/**'
+
+"Port Nuvoton":
+ - changed-files:
+ - any-glob-to-any-file: 'src/portable/nuvoton/**'
+
+"Port RP2":
+ - changed-files:
+ - any-glob-to-any-file: 'src/portable/raspberrypi/**'
+
+"Port MSP430":
+ - changed-files:
+ - any-glob-to-any-file: 'src/portable/ti/msp430x5xx/**'
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index af0191149..a7c7cf99a 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -193,6 +193,36 @@ jobs:
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:
@@ -276,6 +306,9 @@ jobs:
env:
HIL_JSON: ${{ matrix.hil_json }}
steps:
+ - name: Set HIL report dir (sibling of workspace; persists across run attempts)
+ run: echo "HIL_REPORT_DIR=$(dirname "$GITHUB_WORKSPACE")/hil-report" >> "$GITHUB_ENV"
+
- name: Get Skip Boards from previous run
if: github.run_attempt != '1'
run: |
@@ -314,6 +347,15 @@ jobs:
exit 1
fi)
+ - name: Upload HIL report
+ if: always() && github.event_name == 'pull_request'
+ uses: actions/upload-artifact@v7
+ with:
+ name: hil-report-${{ matrix.display }}
+ path: ${{ env.HIL_REPORT_DIR }}/hil_report.md
+ if-no-files-found: ignore
+ overwrite: true
+
# ---------------------------------------
# Hardware in the loop (HIL)
# self-hosted by HFP, build with IAR toolchain, for attached hardware checkout test/hil/hfp.json
@@ -355,8 +397,67 @@ jobs:
run: python3 tools/get_deps.py $BUILD_ARGS
- name: Build
- run: python3 tools/build.py --toolchain iar $BUILD_ARGS
+ run: |
+ # Each variant carries its own --build-name/--cflag, which are global to a
+ # single build.py invocation — so build one matrix entry at a time rather
+ # than joining them (joining would leak a variant's flags onto every board).
+ readarray -t ENTRIES < <(python test/hil/hil_ci_set_matrix.py test/hil/hfp.json | jq -r '.["arm-gcc"][]')
+ for entry in "${ENTRIES[@]}"; do
+ echo "+ tools/build.py --toolchain iar $entry"
+ python3 tools/build.py --toolchain iar $entry
+ done
- name: Test on actual hardware (hardware in the loop)
run: |
python3 test/hil/hil_test.py hfp.json
+
+ - 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
+
+ # ---------------------------------------
+ # Combine HIL results from the rigs into a single sticky PR comment (one table per rig)
+ # ---------------------------------------
+ hil-report:
+ needs: [ hil-tinyusb, hil-hfp-iar ]
+ if: |
+ always() &&
+ (needs.hil-tinyusb.result != 'skipped' || needs.hil-hfp-iar.result != 'skipped') &&
+ github.event_name == 'pull_request' &&
+ github.repository_owner == 'hathach' &&
+ github.event.pull_request.head.repo.fork == false
+ runs-on: ubuntu-latest
+ permissions:
+ pull-requests: write
+ steps:
+ - name: Download HIL reports
+ uses: actions/download-artifact@v5
+ with:
+ pattern: hil-report-*
+ path: hil-reports
+
+ - name: Combine rig reports (one table per rig)
+ run: |
+ {
+ echo "## Hardware-in-the-loop (HIL) Test Report"
+ echo
+ for d in hil-reports/hil-report-*; do
+ [ -d "$d" ] || continue
+ echo "### ${d#hil-reports/hil-report-}"
+ echo
+ cat "$d/hil_report.md" 2>/dev/null || echo "_no report produced_"
+ echo
+ done
+ } > hil_combined.md
+ cat hil_combined.md
+
+ - name: Post HIL report as sticky PR comment
+ uses: marocchino/sticky-pull-request-comment@v2
+ with:
+ header: hil-report
+ path: hil_combined.md
diff --git a/.github/workflows/build_util.yml b/.github/workflows/build_util.yml
index 69b6f28d5..2532caebe 100644
--- a/.github/workflows/build_util.yml
+++ b/.github/workflows/build_util.yml
@@ -67,7 +67,7 @@ jobs:
IAR_LMS_BEARER_TOKEN: ${{ secrets.IAR_LMS_BEARER_TOKEN }}
run: |
if [ "${{ inputs.toolchain }}" == "esp-idf" ]; then
- docker run --rm -e MEMBROWSE_API_KEY="$MEMBROWSE_API_KEY" -v $PWD:/project -w /project espressif/idf:tinyusb python tools/build.py --target all ${{ matrix.arg }}
+ docker run --rm -e MEMBROWSE_API_KEY="$MEMBROWSE_API_KEY" -e CI="$CI" -v $PWD:/project -w /project espressif/idf:tinyusb python tools/build.py --target all ${{ matrix.arg }}
else
BUILD_PY_ARGS="-s ${{ inputs.build-system }} ${{ steps.setup-toolchain.outputs.build_option }} ${{ inputs.build-options }} --target all"
if [ "${{ inputs.upload-metrics }}" = "true" ]; then
diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml
index 43144bb5e..71c1cb8ab 100644
--- a/.github/workflows/claude-code-review.yml
+++ b/.github/workflows/claude-code-review.yml
@@ -1,18 +1,29 @@
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 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:
- if: 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
pull-requests: write
- issues: read
+ issues: write # Claude posts the review comment via the issues API
id-token: write
+ actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
@@ -25,8 +36,22 @@ jobs:
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
+ # Allow claude[bot]'s own pushes to be handled gracefully (skip) instead
+ # of erroring out the workflow
+ allowed_bots: 'claude'
+ # Pairs with the actions: read permission so Claude can read CI results
+ additional_permissions: |
+ actions: read
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
plugins: 'code-review@claude-code-plugins'
- prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
+ # Post/update a single summary comment every run, so a clean review
+ # ("no issues found") is still visible instead of posting nothing.
+ use_sticky_comment: true
+ # --comment makes the code-review command post its findings to the PR.
+ # Without it the command only prints the review to the Actions log.
+ prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }} --comment'
+ # TEMPORARY: expose the full Claude transcript in the Actions log for
+ # debugging. Revert to remove once done.
+ show_full_output: true
+ claude_args: '--max-turns 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..66e36897c 100644
--- a/.github/workflows/claude.yml
+++ b/.github/workflows/claude.yml
@@ -6,22 +6,32 @@ on:
pull_request_review_comment:
types: [created]
issues:
- types: [opened, assigned]
+ # only "opened" — an issue's author_association gates the summon below;
+ # "assigned" would gate on the issue author, not the assigner, so a
+ # maintainer assigning an outsider's issue would be wrongly skipped.
+ types: [opened]
pull_request_review:
types: [submitted]
jobs:
claude:
+ # Only trusted actors (repo owner/member/collaborator) may summon @claude, so the
+ # write-scoped token and OAuth secret are never issued for an outside contributor's
+ # comment on this public repo. Defense-in-depth on top of the action's own check.
if: |
- (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
- (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
- (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
- (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
+ (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude') &&
+ contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)) ||
+ (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude') &&
+ contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)) ||
+ (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude') &&
+ contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.review.author_association)) ||
+ (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')) &&
+ contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.issue.author_association))
runs-on: ubuntu-latest
permissions:
- contents: read
- pull-requests: read
- issues: read
+ contents: write # allow Claude to push commits/branches when asked
+ pull-requests: write # allow Claude to comment on / update PRs
+ issues: write # allow Claude to comment on / update issues
id-token: write
actions: read # Required for Claude to read CI results on PRs
steps:
@@ -40,10 +50,19 @@ jobs:
additional_permissions: |
actions: read
- # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
- # prompt: 'Update the pull request description to include a summary of changes.'
+ # Sign the bot's commits so they show as "Verified". The action commits
+ # automatically — on a PR comment it pushes to that PR's branch; on an
+ # issue comment it opens a new claude/* branch + PR with the fix.
+ use_commit_signing: true
- # Optional: Add claude_args to customize behavior and configuration
- # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
- # or https://code.claude.com/docs/en/cli-reference for available options
- # claude_args: '--allowed-tools Bash(gh pr:*)'
+ # No custom prompt: Claude performs the instructions in the @claude comment.
+
+ # Deliberately NO Bash in the tool allowlist. @claude can be summoned on a
+ # fork PR (claude-code-review.yml even directs fork PRs here), and this job
+ # holds the OAuth secret + a write token. Any build/interpreter command
+ # (python -c, cmake/make custom targets, etc.) run against attacker-
+ # controlled PR content is arbitrary code + network execution, so no
+ # command allowlist can safely contain it. Claude still edits files and
+ # the action commits/opens the PR; the resulting commit is verified by the
+ # repo's CircleCI matrix. --max-turns gives room to investigate + fix.
+ claude_args: '--max-turns 30'
diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml
index c3cc59d0d..fe09413f1 100644
--- a/.github/workflows/labeler.yml
+++ b/.github/workflows/labeler.yml
@@ -4,23 +4,29 @@ on:
issues:
types: [opened]
pull_request_target:
- types: [opened]
+ types: [opened, synchronize, reopened]
+ discussion:
+ types: [created]
jobs:
label-priority:
+ # Author-based priority labels: only on issue/PR/discussion creation, not on PR updates.
+ if: github.event_name != 'pull_request_target' || github.event.action == 'opened'
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
+ discussions: write
steps:
- - name: Label New Issue or PR
- uses: actions/github-script@v7
+ - name: Label New Issue, PR or Discussion
+ uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
- let label = '';
+ let labels = [];
let username = '';
let issueOrPrNumber = 0;
+ let discussionNodeId = '';
if (context.eventName === 'issues') {
username = context.payload.issue.user.login;
@@ -28,25 +34,85 @@ jobs:
} else if (context.eventName === 'pull_request_target') {
username = context.payload.pull_request.user.login;
issueOrPrNumber = context.payload.pull_request.number;
+ } else if (context.eventName === 'discussion') {
+ username = context.payload.discussion.user.login;
+ discussionNodeId = context.payload.discussion.node_id;
}
- // Check if an Adafruit member
- try {
- const adafruitResponse = await github.rest.orgs.checkMembershipForUser({
- org: 'adafruit',
- username: username
- });
+ // Maintainer is an Adafruit member; skip the Adafruit perks for their own
+ // issues/PRs and treat them as a plain contributor (Prio only).
+ const isOwner = username.toLowerCase() === 'hathach';
- if (adafruitResponse.status === 204) {
- console.log('Adafruit Member');
- label = 'Prio Urgent';
+ // Check if an Adafruit member: Adafruit + Sponsor + top priority
+ if (!isOwner) {
+ try {
+ const adafruitResponse = await github.rest.orgs.checkMembershipForUser({
+ org: 'adafruit',
+ username: username
+ });
+
+ if (adafruitResponse.status === 204) {
+ console.log('Adafruit Member');
+ labels = ['Adafruit 🌸', 'Sponsor 💖', 'Prio Top 🚨'];
+ }
+ } catch (error) {
+ console.log('Not an Adafruit member');
}
- } catch (error) {
- console.log('Not an Adafruit member');
}
- // Check if a contributor
- if (label == '') {
+ // Check if a public GitHub Sponsor of the repo owner.
+ // Word ($32) tier and up get triage priority; DWORD/QWORD ($128+) go to the top.
+ // Private sponsorships are not visible to GITHUB_TOKEN, so only public sponsors are detected.
+ if (labels.length === 0) {
+ try {
+ const result = await github.graphql(`
+ query($sponsorable: String!, $sponsor: String!) {
+ user(login: $sponsorable) {
+ isSponsoredBy(accountLogin: $sponsor)
+ sponsorshipsAsMaintainer(includePrivate: false, first: 100) {
+ nodes {
+ sponsorEntity {
+ ... on User { login }
+ ... on Organization { login }
+ }
+ tier { monthlyPriceInDollars }
+ }
+ }
+ }
+ }`, { sponsorable: context.repo.owner, sponsor: username });
+
+ const owner = result.user;
+ if (owner && owner.isSponsoredBy) {
+ let monthly = 0;
+ const nodes = (owner.sponsorshipsAsMaintainer && owner.sponsorshipsAsMaintainer.nodes) || [];
+ for (const node of nodes) {
+ const login = node.sponsorEntity && node.sponsorEntity.login;
+ if (login && login.toLowerCase() === username.toLowerCase()) {
+ monthly = (node.tier && node.tier.monthlyPriceInDollars) || 0;
+ break;
+ }
+ }
+
+ if (monthly >= 128) {
+ console.log('Sponsor (DWORD/QWORD tier)');
+ labels = ['Sponsor 💖', 'Prio Top 🚨'];
+ } else if (monthly >= 32) {
+ console.log('Sponsor (Word tier)');
+ labels = ['Sponsor 💖', 'Prio 📌'];
+ } else {
+ console.log('Sponsor (below Word tier or tier not visible)');
+ labels = ['Sponsor 💖'];
+ }
+ } else {
+ console.log('Not a public sponsor');
+ }
+ } catch (error) {
+ console.log('Sponsor lookup failed: ' + error.message);
+ }
+ }
+
+ // Check if a contributor: prioritized in triage queue
+ if (labels.length === 0) {
try {
const collaboratorResponse = await github.rest.repos.checkCollaborator({
owner: context.repo.owner,
@@ -56,18 +122,61 @@ jobs:
if (collaboratorResponse.status === 204) {
console.log('Contributor');
- label = 'Prio Higher';
+ labels = ['Prio 📌'];
}
} catch (error) {
console.log('Not a contributor');
}
}
- if (label !== '') {
- await github.rest.issues.addLabels({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: issueOrPrNumber,
- labels: [label]
- });
+ if (labels.length !== 0) {
+ if (context.eventName === 'discussion') {
+ // Discussions are not covered by the REST issues API; resolve the label
+ // names to node IDs and attach them with the GraphQL labelable mutation.
+ const labelIds = [];
+ for (const name of labels) {
+ const res = await github.graphql(`
+ query($owner: String!, $repo: String!, $name: String!) {
+ repository(owner: $owner, name: $repo) {
+ label(name: $name) { id }
+ }
+ }`, { owner: context.repo.owner, repo: context.repo.repo, name: name });
+ if (res.repository.label) {
+ labelIds.push(res.repository.label.id);
+ }
+ }
+ if (labelIds.length !== 0) {
+ await github.graphql(`
+ mutation($labelableId: ID!, $labelIds: [ID!]!) {
+ addLabelsToLabelable(input: { labelableId: $labelableId, labelIds: $labelIds }) {
+ clientMutationId
+ }
+ }`, { labelableId: discussionNodeId, labelIds: labelIds });
+ }
+ } else {
+ await github.rest.issues.addLabels({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: issueOrPrNumber,
+ labels: labels
+ });
+ }
}
+
+ # Path-based Port labels: attach "Port <ip>" when a PR touches the matching
+ # dcd/hcd driver under src/portable/. Mapping lives in .github/labeler.yml.
+ label-port:
+ if: github.event_name == 'pull_request_target'
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ pull-requests: write
+ issues: write # allow auto-creating a Port label that doesn't exist yet
+ steps:
+ - uses: actions/labeler@v5
+ with:
+ configuration-path: .github/labeler.yml
+ # sync-labels so a Port label is removed once a PR no longer touches
+ # that driver (job reruns on synchronize). Only labels listed in
+ # labeler.yml are managed, so author-based Prio/Sponsor labels are untouched.
+ sync-labels: true