[PATCH BlueZ v1 1/3] github: Add YAML issue template and btsnoop-analyzer workflow

Luiz Augusto von Dentz
2026-04-15T18:22:21Z

Replace the markdown issue template with a structured YAML form that
includes fields for description, reproduction steps, btmon trace upload,
analysis focus area selection, and privacy consent checkboxes.

Add a btsnoop-analyzer workflow that automatically analyzes btsnoop
traces attached to new issues using Vudentz/btsnoop-analyzer action,
posting results as issue comments.
---
.github/ISSUE_TEMPLATE/issue.md | 41 -----
.github/ISSUE_TEMPLATE/issue.yml | 108 +++++++++++++
.github/workflows/btsnoop-analyzer.yml | 202 +++++++++++++++++++++++++
3 files changed, 310 insertions(+), 41 deletions(-)
delete mode 100644 .github/ISSUE_TEMPLATE/issue.md
create mode 100644 .github/ISSUE_TEMPLATE/issue.yml
create mode 100644 .github/workflows/btsnoop-analyzer.yml

diff --git a/.github/ISSUE_TEMPLATE/issue.md b/.github/ISSUE_TEMPLATE/issue.md
deleted file mode 100644
index ba384e120bac..000000000000
--- a/.github/ISSUE_TEMPLATE/issue.md
+++ /dev/null
@@ -1,41 +0,0 @@
----
-name: New issue
-about: 'Report a bug or other problem'
-title: ''
-labels: ''
-assignees: ''
-
----
-
-### Description
-
-
-
-
-
-
-
-### To reproduce
-
-1.
-2.
-3.
-4.
-
-### Logs
-- btmon log: <!-- Bluetooth traffic dump: (usually needed)
- 0. Power off connected device
- 1. Run: btmon -w btmon.log
- 2. Connect device, reproduce the issue
- 3. Ctrl-C btmon
- 4. Attach btmon.log to the issue -->
-- bluetoothd log: <!-- Run: journalctl -u bluetooth --boot 0 > bluetoothd.log; if relevant for issue -->
-
-
-
-### Versions
-- BlueZ version:
-- Kernel version:
-- Problematic device:
-
-
diff --git a/.github/ISSUE_TEMPLATE/issue.yml b/.github/ISSUE_TEMPLATE/issue.yml
new file mode 100644
index 000000000000..0f069858eeaf
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/issue.yml
@@ -0,0 +1,108 @@
+name: New issue
+description: Report a bug or other problem
+labels: []
+body:
+ - type: textarea
+ id: description
+ attributes:
+ label: Description
+ description: |
+ A clear and concise description of the bug, what you expected
+ to happen, and any other relevant details.
+ placeholder: Describe the issue...
+ validations:
+ required: true
+
+ - type: textarea
+ id: reproduce
+ attributes:
+ label: To reproduce
+ description: Steps to reproduce the issue, if possible.
+ placeholder: |
+ 1.
+ 2.
+ 3.
+ 4.
+ validations:
+ required: false
+
+ - type: textarea
+ id: trace-file
+ attributes:
+ label: btmon trace
+ description: |
+ Drag and drop your btsnoop trace file here. If a trace is
+ attached and the privacy acknowledgment below is checked,
+ it will be automatically analyzed by
+ [btsnoop-analyzer](https://github.com/Vudentz/btsnoop-analyzer).
+
+ To capture a trace: power off the device, run `btmon -w btmon.log`,
+ reproduce the issue, then Ctrl-C btmon.
+ placeholder: Drag and drop your btmon.log file here...
+ validations:
+ required: false
+
+ - type: dropdown
+ id: focus-area
+ attributes:
+ label: Analysis focus
+ description: >
+ If you attached a btsnoop trace, what aspect should the
+ automated analysis focus on? Select "General" to auto-detect.
+ options:
+ - General (full analysis)
+ - Connection issues
+ - Controller enumeration
+ - Pairing / Security
+ - GATT discovery
+ - Audio
+ - Audio / LE Audio
+ - Audio / A2DP
+ - Audio / HFP
+ - L2CAP channel issues
+ - Advertising / Scanning
+ - Disconnection analysis
+ - Channel Sounding
+ validations:
+ required: false
+
+ - type: checkboxes
+ id: privacy
+ attributes:
+ label: Privacy
+ description: |
+ btsnoop traces contain Bluetooth MAC addresses and may contain
+ device names. By default, MAC addresses are anonymized before
+ being sent to an LLM for analysis.
+ options:
+ - label: >
+ **Skip anonymization** — Send the raw decoded trace to the
+ LLM without scrubbing MAC addresses or device names.
+ required: false
+ - label: >
+ I understand this trace will be processed by a third-party
+ LLM API and I have the right to share this data.
+ required: false
+
+ - type: textarea
+ id: logs
+ attributes:
+ label: Other logs
+ description: |
+ Attach bluetoothd or other relevant logs.
+ Run: `journalctl -u bluetooth --boot 0 > bluetoothd.log`
+ render: text
+ validations:
+ required: false
+
+ - type: textarea
+ id: versions
+ attributes:
+ label: Versions
+ description: BlueZ version, kernel version, and device information.
+ value: |
+ - BlueZ version:
+ - Kernel version:
+ - Problematic device:
+ validations:
+ required: false
diff --git a/.github/workflows/btsnoop-analyzer.yml b/.github/workflows/btsnoop-analyzer.yml
new file mode 100644
index 000000000000..3bbd963004cb
--- /dev/null
+++ b/.github/workflows/btsnoop-analyzer.yml
@@ -0,0 +1,202 @@
+name: btsnoop-analyzer
+
+on:
+ issues:
+ types: [opened, reopened]
+
+permissions:
+ issues: write
+ contents: read
+
+jobs:
+ analyze:
+ # Only run when the user acknowledged the privacy statement
+ if: contains(github.event.issue.body, 'I understand this trace will be processed')
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+
+ steps:
+ - name: Parse issue body
+ id: parse
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const body = context.payload.issue.body || '';
+
+ // Extract trace file URL from issue body
+ const urlPatterns = [
+ /https:\/\/github\.com\/[^\s)]+\.(?:log|snoop|btsnoop|cfa)/gi,
+ /https:\/\/github\.com\/user-attachments\/(?:files|assets)\/[^\s)]+/gi,
+ ];
+
+ let traceUrl = '';
+ for (const pattern of urlPatterns) {
+ const match = body.match(pattern);
+ if (match) {
+ traceUrl = match[0];
+ break;
+ }
+ }
+
+ if (!traceUrl) {
+ console.log('No trace file URL found — skipping analysis');
+ core.setOutput('found', 'false');
+ return;
+ }
+
+ // Extract description
+ const descMatch = body.match(
+ /### Description\s*\n([\s\S]*?)(?=\n###|\n\*\*|$)/i
+ );
+ const description = descMatch?.[1]?.trim() || 'No description provided';
+
+ // Extract focus area
+ const focusMatch = body.match(
+ /### Analysis focus\s*\n\s*(\S[^\n]*)/i
+ );
+ const focus = focusMatch?.[1]?.trim() || 'General (full analysis)';
+
+ // Check anonymization preference
+ const skipAnon = body.includes('[X] **Skip anonymization**') ||
+ body.includes('[x] **Skip anonymization**');
+
+ core.setOutput('found', 'true');
+ core.setOutput('trace_url', traceUrl);
+ core.setOutput('description', description);
+ core.setOutput('focus', focus);
+ core.setOutput('anonymize', skipAnon ? 'false' : 'true');
+
+ console.log(`Trace URL: ${traceUrl}`);
+ console.log(`Focus: ${focus}`);
+ console.log(`Anonymize: ${!skipAnon}`);
+
+ - name: Post "analyzing" comment
+ if: steps.parse.outputs.found == 'true'
+ uses: actions/github-script@v7
+ with:
+ script: |
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ body: '**btsnoop Analyzer** is processing your trace. This typically takes 1-3 minutes.\n\n_Decoding with btmon, then sending to LLM for analysis..._'
+ });
+
+ - name: Run btsnoop-analyzer
+ if: steps.parse.outputs.found == 'true'
+ id: analysis
+ uses: Vudentz/btsnoop-analyzer@master
+ with:
+ trace-url: ${{ steps.parse.outputs.trace_url }}
+ description: ${{ steps.parse.outputs.description }}
+ focus: ${{ steps.parse.outputs.focus }}
+ anonymize: ${{ steps.parse.outputs.anonymize }}
+ provider: ${{ vars.LLM_PROVIDER || 'github' }}
+ model: ${{ vars.LLM_MODEL || '' }}
+ env:
+ OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ GH_MODELS_TOKEN: ${{ secrets.GH_MODELS_TOKEN }}
+
+ - name: Post detection comment
+ if: steps.parse.outputs.found == 'true' && success()
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const fs = require('fs');
+ const detect = fs.readFileSync('${{ steps.analysis.outputs.detect }}', 'utf8');
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ body: detect
+ });
+
+ - name: Post filter comment
+ if: steps.parse.outputs.found == 'true' && success()
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const fs = require('fs');
+ const filter = fs.readFileSync('${{ steps.analysis.outputs.filter }}', 'utf8');
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ body: filter
+ });
+
+ - name: Post annotation comment
+ if: steps.parse.outputs.found == 'true' && success()
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const fs = require('fs');
+ const annotate = fs.readFileSync('${{ steps.analysis.outputs.annotate }}', 'utf8');
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ body: annotate
+ });
+
+ - name: Post diagnostics comment
+ if: steps.parse.outputs.found == 'true' && success()
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const fs = require('fs');
+ const diagnose = fs.readFileSync('${{ steps.analysis.outputs.diagnose }}', 'utf8');
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ body: diagnose
+ });
+
+ - name: Post analysis comment
+ if: steps.parse.outputs.found == 'true' && success()
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const fs = require('fs');
+ const analysis = fs.readFileSync('${{ steps.analysis.outputs.analyze }}', 'utf8');
+
+ const footer = `\n\n---\nAnalyzed by [btsnoop-analyzer](https://github.com/Vudentz/btsnoop-analyzer) using btmon from [BlueZ](https://github.com/bluez/bluez). MAC addresses ${
+ '${{ steps.parse.outputs.anonymize }}' === 'true'
+ ? 'were anonymized'
+ : 'were **not** anonymized (user opted out)'
+ } before LLM processing.`;
+
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ body: analysis + footer
+ });
+
+ - name: Post error comment
+ if: steps.parse.outputs.found == 'true' && failure()
+ uses: actions/github-script@v7
+ with:
+ script: |
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ body: `## Analysis Failed
+
+ The automated analysis encountered an error. This could be due to:
+ - Unsupported trace file format
+ - Trace file too large to process
+ - LLM API rate limiting or downtime
+
+ A maintainer will review your trace manually.
+
+
+ Debug info
+
+ Workflow run: https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}
+ `
+ });
--
2.53.0