diff --git a/.github/workflows/models_completeness.yml b/.github/workflows/models_completeness.yml deleted file mode 100644 index 8da879311..000000000 --- a/.github/workflows/models_completeness.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: Issue Completeness Check - -on: - issues: - types: [opened] - -permissions: - issues: write - models: read - -jobs: - check-completeness: - runs-on: ubuntu-latest - steps: - - name: Check if should skip - uses: actions/github-script@v8 - id: check-skip - with: - script: | - const title = context.payload.issue.title.toLowerCase(); - const labels = context.payload.issue.labels.map(label => label.name); - const hasFeatureRequest = title.includes('feature request'); - const hasEnhancement = labels.includes('enhancement'); - const shouldSkip = hasFeatureRequest && hasEnhancement; - core.setOutput('should_skip', shouldSkip); - - - name: Check issue completeness - if: steps.check-skip.outputs.should_skip != 'true' - uses: actions/ai-inference@v2 - id: ai - with: - prompt: | - Analyze this GitHub issue for completeness. If missing reproduction steps, version info, or expected/actual behavior, respond with a friendly request for the missing info. If complete, say so. - - Title: ${{ github.event.issue.title }} - Body: ${{ github.event.issue.body }} - system-prompt: You are a helpful assistant that helps analyze GitHub issues for completeness. - model: openai/gpt-4o-mini - # temperature: 0.2 - - - name: Comment on issue - if: steps.check-skip.outputs.should_skip != 'true' && steps.ai.outputs.response != '' - uses: actions/github-script@v8 - with: - script: | - github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: ${{ github.event.issue.number }}, - body: `${{ steps.ai.outputs.response }}` - }) \ No newline at end of file diff --git a/.github/workflows/models_dedupe.yml b/.github/workflows/models_dedupe.yml deleted file mode 100644 index a25728f1b..000000000 --- a/.github/workflows/models_dedupe.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Detect duplicate issues - -on: - issues: - types: [opened, reopened] - -permissions: - models: read - issues: write - -concurrency: - group: ${{ github.workflow }}-${{ github.event.issue.number }} - cancel-in-progress: true - -jobs: - continuous-triage-dedup: - if: ${{ github.event.issue.user.type != 'Bot' }} - runs-on: ubuntu-latest - steps: - - uses: pelikhan/action-genai-issue-dedup@v0 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - # Optional tuning: - # labels: "auto" # compare within matching labels, or "bug,api" - # count: "20" # how many recent issues to check - # since: "90d" # look back window, supports d/w/m \ No newline at end of file diff --git a/.github/workflows/models_issue_triage.yml b/.github/workflows/models_issue_triage.yml new file mode 100644 index 000000000..f61a15fe6 --- /dev/null +++ b/.github/workflows/models_issue_triage.yml @@ -0,0 +1,204 @@ +name: Issue Triage (Models) + +on: + issues: + types: [opened] + +permissions: + issues: write + models: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.issue.number }} + cancel-in-progress: true + +jobs: + triage: + if: ${{ github.repository == 'meshtastic/firmware' && github.event.issue.user.type != 'Bot' }} + runs-on: ubuntu-latest + steps: + # ───────────────────────────────────────────────────────────────────────── + # Step 1: Quality check (spam/AI-slop detection) - runs first, exits early if spam + # ───────────────────────────────────────────────────────────────────────── + - name: Detect spam or low-quality content + uses: actions/ai-inference@v2 + id: quality + continue-on-error: true + with: + max-tokens: 20 + prompt: | + Is this GitHub issue spam, AI-generated slop, or low quality? + + Title: ${{ github.event.issue.title }} + Body: ${{ github.event.issue.body }} + + Respond with exactly one of: spam, ai-generated, needs-review, ok + system-prompt: You detect spam and low-quality contributions. Be conservative - only flag obvious spam or AI slop. + model: openai/gpt-4o-mini + + - name: Apply quality label if needed + if: steps.quality.outputs.response != '' && steps.quality.outputs.response != 'ok' + uses: actions/github-script@v8 + env: + QUALITY_LABEL: ${{ steps.quality.outputs.response }} + with: + script: | + const label = (process.env.QUALITY_LABEL || '').trim().toLowerCase(); + const labelMeta = { + 'spam': { color: 'd73a4a', description: 'Possible spam' }, + 'ai-generated': { color: 'fbca04', description: 'Possible AI-generated low-quality content' }, + 'needs-review': { color: 'f9d0c4', description: 'Needs human review' }, + }; + const meta = labelMeta[label]; + if (!meta) return; + + // Ensure label exists + try { + await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label }); + } catch (e) { + if (e.status !== 404) throw e; + await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description }); + } + + // Apply label + await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.issue.number, labels: [label] }); + + // Set output to skip remaining steps + core.setOutput('is_spam', 'true'); + + # ───────────────────────────────────────────────────────────────────────── + # Step 2: Duplicate detection - only if not spam + # ───────────────────────────────────────────────────────────────────────── + - name: Detect duplicate issues + if: steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '' + uses: pelikhan/action-genai-issue-dedup@bdb3b5d9451c1090ffcdf123d7447a5e7c7a2528 # v0.0.19 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + + # ───────────────────────────────────────────────────────────────────────── + # Step 3: Completeness check + auto-labeling (combined into one AI call) + # ───────────────────────────────────────────────────────────────────────── + - name: Determine if completeness check should be skipped + if: steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '' + uses: actions/github-script@v8 + id: check-skip + with: + script: | + const title = (context.payload.issue.title || '').toLowerCase(); + const labels = (context.payload.issue.labels || []).map(label => label.name); + const hasFeatureRequest = title.includes('feature request'); + const hasEnhancement = labels.includes('enhancement'); + const shouldSkip = hasFeatureRequest && hasEnhancement; + core.setOutput('should_skip', shouldSkip ? 'true' : 'false'); + + - name: Analyze issue completeness and determine labels + if: (steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '') && steps.check-skip.outputs.should_skip != 'true' + uses: actions/ai-inference@v2 + id: analysis + continue-on-error: true + with: + prompt: | + Analyze this GitHub issue for completeness and determine if it needs labels. + + If this looks like a bug on the device/firmware (crash, reboot, lockup, radio issues, GPS issues, display issues, power/sleep issues), request device logs and explain how to get them: + + Web Flasher logs: + - Go to https://flasher.meshtastic.org + - Connect the device via USB and click Connect + - Open the device console/log output, reproduce the problem, then copy/download and attach/paste the logs + + Meshtastic CLI logs: + - Run: meshtastic --port --noproto + - Reproduce the problem, then copy/paste the terminal output + + Also request key context if missing: device model/variant, firmware version, region, steps to reproduce, expected vs actual. + + Respond ONLY with JSON: + { + "complete": true|false, + "comment": "Your helpful comment requesting missing info, or empty string if complete", + "label": "needs-logs" | "needs-info" | "none" + } + + Use "needs-logs" if this is a device bug AND no logs are attached. + Use "needs-info" if basic info like firmware version or steps to reproduce are missing. + Use "none" if the issue is complete or is a feature request. + + Title: ${{ github.event.issue.title }} + Body: ${{ github.event.issue.body }} + system-prompt: You are a helpful assistant that triages GitHub issues. Be conservative with labels. + model: openai/gpt-4o-mini + + - name: Process analysis result + if: (steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '') && steps.check-skip.outputs.should_skip != 'true' && steps.analysis.outputs.response != '' + uses: actions/github-script@v8 + id: process + env: + AI_RESPONSE: ${{ steps.analysis.outputs.response }} + with: + script: | + const raw = (process.env.AI_RESPONSE || '').trim(); + + let complete = false; + let comment = ''; + let label = 'none'; + + try { + const parsed = JSON.parse(raw); + complete = !!parsed.complete; + comment = (parsed.comment ?? '').toString().trim(); + label = (parsed.label ?? 'none').toString().trim().toLowerCase(); + } catch { + // If JSON parse fails, treat as incomplete with raw response as comment + complete = false; + comment = raw; + label = 'none'; + } + + // Validate label + const allowedLabels = new Set(['needs-logs', 'needs-info', 'none']); + if (!allowedLabels.has(label)) label = 'none'; + + core.setOutput('should_comment', (!complete && comment.length > 0) ? 'true' : 'false'); + core.setOutput('comment_body', comment); + core.setOutput('label', label); + + - name: Apply triage label + if: steps.process.outputs.label != '' && steps.process.outputs.label != 'none' + uses: actions/github-script@v8 + env: + LABEL_NAME: ${{ steps.process.outputs.label }} + with: + script: | + const label = process.env.LABEL_NAME; + const labelMeta = { + 'needs-logs': { color: 'cfd3d7', description: 'Device logs requested for triage' }, + 'needs-info': { color: 'f9d0c4', description: 'More information requested for triage' }, + }; + const meta = labelMeta[label]; + if (!meta) return; + + // Ensure label exists + try { + await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label }); + } catch (e) { + if (e.status !== 404) throw e; + await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description }); + } + + // Apply label + await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.issue.number, labels: [label] }); + + - name: Comment on issue + if: steps.process.outputs.should_comment == 'true' + uses: actions/github-script@v8 + env: + COMMENT_BODY: ${{ steps.process.outputs.comment_body }} + with: + script: | + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.issue.number, + body: process.env.COMMENT_BODY + }); diff --git a/.github/workflows/models_onboarding.yml b/.github/workflows/models_onboarding.yml deleted file mode 100644 index b700f81eb..000000000 --- a/.github/workflows/models_onboarding.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Continuous AI Resolver - - -on: - schedule: - - cron: '0 0 * * 0' # Runs every Sunday at midnight UTC - workflow_dispatch: - - -permissions: - issues: write - pull-requests: write - - -jobs: - resolver: - runs-on: ubuntu-latest - steps: - - name: Run resolver - uses: ashleywolf/continuous-ai-resolver@main - with: - github_token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/models_pr_triage.yml b/.github/workflows/models_pr_triage.yml new file mode 100644 index 000000000..ef303c02a --- /dev/null +++ b/.github/workflows/models_pr_triage.yml @@ -0,0 +1,138 @@ +name: PR Triage (Models) + +on: + pull_request_target: + types: [opened] + +permissions: + pull-requests: write + issues: write + models: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + triage: + if: ${{ github.repository == 'meshtastic/firmware' && github.event.pull_request.user.type != 'Bot' }} + runs-on: ubuntu-latest + steps: + # ───────────────────────────────────────────────────────────────────────── + # Step 1: Check if PR already has automation/type labels (skip if so) + # ───────────────────────────────────────────────────────────────────────── + - name: Check existing labels + uses: actions/github-script@v8 + id: check-labels + with: + script: | + const skipLabels = new Set(['automation']); + const typeLabels = new Set(['bugfix', 'hardware-support', 'enhancement', 'dependencies', 'submodules', 'github_actions', 'trunk', 'cleanup']); + const prLabels = context.payload.pull_request.labels.map(l => l.name); + + const shouldSkipAll = prLabels.some(l => skipLabels.has(l)); + const hasTypeLabel = prLabels.some(l => typeLabels.has(l)); + + core.setOutput('skip_all', shouldSkipAll ? 'true' : 'false'); + core.setOutput('has_type_label', hasTypeLabel ? 'true' : 'false'); + + # ───────────────────────────────────────────────────────────────────────── + # Step 2: Quality check (spam/AI-slop detection) + # ───────────────────────────────────────────────────────────────────────── + - name: Detect spam or low-quality content + if: steps.check-labels.outputs.skip_all != 'true' + uses: actions/ai-inference@v2 + id: quality + continue-on-error: true + with: + max-tokens: 20 + prompt: | + Is this GitHub pull request spam, AI-generated slop, or low quality? + + Title: ${{ github.event.pull_request.title }} + Body: ${{ github.event.pull_request.body }} + + Respond with exactly one of: spam, ai-generated, needs-review, ok + system-prompt: You detect spam and low-quality contributions. Be conservative - only flag obvious spam or AI slop. + model: openai/gpt-4o-mini + + - name: Apply quality label if needed + if: steps.check-labels.outputs.skip_all != 'true' && steps.quality.outputs.response != '' && steps.quality.outputs.response != 'ok' + uses: actions/github-script@v8 + id: quality-label + env: + QUALITY_LABEL: ${{ steps.quality.outputs.response }} + with: + script: | + const label = (process.env.QUALITY_LABEL || '').trim().toLowerCase(); + const labelMeta = { + 'spam': { color: 'd73a4a', description: 'Possible spam' }, + 'ai-generated': { color: 'fbca04', description: 'Possible AI-generated low-quality content' }, + 'needs-review': { color: 'f9d0c4', description: 'Needs human review' }, + }; + const meta = labelMeta[label]; + if (!meta) return; + + // Ensure label exists + try { + await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label }); + } catch (e) { + if (e.status !== 404) throw e; + await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description }); + } + + // Apply label + await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.pull_request.number, labels: [label] }); + + core.setOutput('is_spam', 'true'); + + # ───────────────────────────────────────────────────────────────────────── + # Step 3: Auto-label PR type (bugfix/hardware-support/enhancement) + # ───────────────────────────────────────────────────────────────────────── + - name: Classify PR for labeling + if: steps.check-labels.outputs.skip_all != 'true' && steps.check-labels.outputs.has_type_label != 'true' && (steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '') + uses: actions/ai-inference@v2 + id: classify + continue-on-error: true + with: + max-tokens: 30 + prompt: | + Classify this pull request into exactly one category. + + Return exactly one of: bugfix, hardware-support, enhancement + + Use bugfix if it fixes a bug, crash, or incorrect behavior. + Use hardware-support if it adds or improves support for a specific hardware device/variant. + Use enhancement if it adds a new feature, improves performance, or refactors code. + + Title: ${{ github.event.pull_request.title }} + Body: ${{ github.event.pull_request.body }} + system-prompt: You classify pull requests into categories. Be conservative and pick the most appropriate single label. + model: openai/gpt-4o-mini + + - name: Apply type label + if: steps.check-labels.outputs.skip_all != 'true' && steps.check-labels.outputs.has_type_label != 'true' && steps.classify.outputs.response != '' + uses: actions/github-script@v8 + env: + TYPE_LABEL: ${{ steps.classify.outputs.response }} + with: + script: | + const label = (process.env.TYPE_LABEL || '').trim().toLowerCase(); + const labelMeta = { + 'bugfix': { color: 'd73a4a', description: 'Bug fix' }, + 'hardware-support': { color: '0e8a16', description: 'Hardware support addition or improvement' }, + 'enhancement': { color: 'a2eeef', description: 'New feature or enhancement' }, + }; + const meta = labelMeta[label]; + if (!meta) return; + + // Ensure label exists + try { + await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label }); + } catch (e) { + if (e.status !== 404) throw e; + await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description }); + } + + // Apply label + await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.pull_request.number, labels: [label] }); diff --git a/.github/workflows/models_quality.yml b/.github/workflows/models_quality.yml deleted file mode 100644 index d612471d6..000000000 --- a/.github/workflows/models_quality.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Contribution Quality Check - -on: - pull_request: - types: [opened] - issues: - types: [opened] - -permissions: - pull-requests: write - issues: write - models: read - -jobs: - quality-check: - runs-on: ubuntu-latest - if: ${{ github.event_name == 'issues' || (github.event_name == 'pull_request' && github.event.pull_request.labels == null || !contains(join(github.event.pull_request.labels.*.name, ','), 'automation')) }} - steps: - - name: Detect spam or low-quality content - uses: actions/ai-inference@v2 - id: ai - with: - prompt: | - Is this GitHub ${{ github.event_name == 'issues' && 'issue' || 'pull request' }} spam, AI-generated slop, or low quality? - - Title: ${{ github.event.issue.title || github.event.pull_request.title }} - Body: ${{ github.event.issue.body || github.event.pull_request.body }} - - Respond with one of: spam, ai-generated, needs-review, or ok - system-prompt: You detect spam and low-quality contributions. Be conservative - only flag obvious spam or AI slop. - model: openai/gpt-4o-mini - # temperature: 0.1 - - - name: Apply label if needed - if: steps.ai.outputs.response != 'ok' - uses: actions/github-script@v8 - with: - script: | - const label = `${{ steps.ai.outputs.response }}`; - const number = ${{ github.event.issue.number || github.event.pull_request.number }}; - - if (label && label !== 'ok') { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: number, - labels: [label] - }); - } diff --git a/.github/workflows/models_resolver.yml b/.github/workflows/models_resolver.yml deleted file mode 100644 index 798b62b62..000000000 --- a/.github/workflows/models_resolver.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Welcome New Contributors - -on: - pull_request: - types: [opened] - -permissions: - pull-requests: write - models: read - -jobs: - welcome: - runs-on: ubuntu-latest - if: github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' - steps: - - name: Generate welcome message - uses: actions/ai-inference@v2 - id: ai - with: - prompt: | - Write a friendly welcome message for a first-time contributor. Include: - 1. Thank them for their first PR - 2. Mention checking CONTRIBUTING.md - 3. Offer to help if they have questions - - Keep it brief and encouraging. - model: openai/gpt-4o-mini - temperature: 0.7 - - - name: Post welcome comment - uses: actions/github-script@v8 - with: - script: | - const message = `${{ steps.ai.outputs.response }}`; - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: ${{ github.event.pull_request.number }}, - body: message - }); \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 12da04656..aca967979 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -81,16 +81,16 @@ develocity { local { isEnabled = true } -// remote(HttpBuildCache::class.java) { -// isAllowInsecureProtocol = true -// // Replace with your selfhosted instance address -// // see: https://docs.gradle.org/current/userguide/build_cache.html#sec:build_cache_setup_http_backend -// url = uri("http://:5071/cache/") -// -// // Allow this machine to upload results to the cache -// isPush = true -// -// } + remote(HttpBuildCache::class.java) { + isAllowInsecureProtocol = true + // Replace with your selfhosted instance address + // see: https://docs.gradle.org/current/userguide/build_cache.html#sec:build_cache_setup_http_backend + url = uri("http://192.168.1.3:5071/cache/") + + // Allow this machine to upload results to the cache + isPush = true + + } } }