From 07975052daf37675358098b38e1d558f3df42c8e Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Sun, 28 Jun 2026 19:53:48 -0500 Subject: [PATCH] chore(ci): prune dead workflows, move vuln scanning to Renovate, fix changelog gaps (#6000) Co-authored-by: Claude Opus 4.8 --- .github/release.yml | 2 - .github/renovate.json | 1 + .github/workflows/dependency-submission.yml | 29 -- .github/workflows/docs-governance.yml | 419 -------------------- .github/workflows/models_issue_triage.yml | 203 ---------- .github/workflows/models_pr_triage.yml | 159 -------- .github/workflows/moderate.yml | 26 -- .github/workflows/update-changelog.yml | 30 +- 8 files changed, 8 insertions(+), 861 deletions(-) delete mode 100644 .github/workflows/dependency-submission.yml delete mode 100644 .github/workflows/docs-governance.yml delete mode 100644 .github/workflows/models_issue_triage.yml delete mode 100644 .github/workflows/models_pr_triage.yml delete mode 100644 .github/workflows/moderate.yml diff --git a/.github/release.yml b/.github/release.yml index c029baaf9..0ad30dc31 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -2,7 +2,6 @@ # # Labels here must match actual repo labels. Run `gh label list` to verify. # Auto-labeler: .github/workflows/pull-request-target.yml -# AI classifier: .github/workflows/models_pr_triage.yml changelog: exclude: @@ -16,7 +15,6 @@ changelog: - ci - build - testing - - refactor - documentation - l10n authors: diff --git a/.github/renovate.json b/.github/renovate.json index ba277db37..d2dfea94e 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -9,6 +9,7 @@ "workarounds:all" ], "commitMessageTopic": "{{depName}}", + "osvVulnerabilityAlerts": true, "labels": [ "dependencies" ], diff --git a/.github/workflows/dependency-submission.yml b/.github/workflows/dependency-submission.yml deleted file mode 100644 index 5a8d203c3..000000000 --- a/.github/workflows/dependency-submission.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: Dependency Submission - -on: - push: - branches: [ 'main' ] - workflow_dispatch: - -permissions: - contents: write - -jobs: - dependency-submission: - runs-on: ubuntu-24.04 - if: github.repository == 'meshtastic/Meshtastic-Android' - - steps: - - uses: actions/checkout@v7.0.0 - - uses: actions/setup-java@v5 - with: - distribution: temurin - java-version: 21 - token: ${{ github.token }} - - - name: Generate and submit dependency graph - uses: gradle/actions/dependency-submission@v6 - with: - build-scan-publish: true - build-scan-terms-of-use-url: "https://gradle.com/terms-of-service" - build-scan-terms-of-use-agree: "yes" diff --git a/.github/workflows/docs-governance.yml b/.github/workflows/docs-governance.yml deleted file mode 100644 index 4f014b3de..000000000 --- a/.github/workflows/docs-governance.yml +++ /dev/null @@ -1,419 +0,0 @@ -name: UI & Docs Governance - -# Split by trust level: -# - pull_request (fork context, read-only token, NO secrets) runs the `validate` -# job, which checks out and EXECUTES fork-supplied code (node scripts/*.js). This -# is safe only because the token is read-only and secrets are unavailable. -# - pull_request_target (base context, write token) runs the comment-posting -# staleness jobs. They MUST NOT check out or execute fork code — they read the -# changed-file list via the API instead. (actions/checkout@v7 also refuses a fork -# checkout under pull_request_target, which is what surfaced this.) -on: - pull_request: - branches: [main] - types: [opened, synchronize, reopened] - pull_request_target: - branches: [main] - types: [opened, synchronize, reopened, labeled, unlabeled] - schedule: - - cron: "0 6 * * 1" # Every Monday at 6 AM UTC - workflow_dispatch: - -permissions: - contents: read - pull-requests: write - -jobs: - # ── Job 1: Staleness — flag PRs that change UI without updating docs ──────── - staleness: - name: Docs staleness check - runs-on: ubuntu-24.04-arm - # pull_request_target only: needs the write token to post advisory comments. - # Reads the changed-file list via the API — no fork checkout, no fork code run. - if: >- - github.event_name == 'pull_request_target' - && github.event.pull_request != null - && !contains(github.event.pull_request.labels.*.name, 'skip-docs-check') - - steps: - - name: Detect changed files - id: changed - env: - GH_TOKEN: ${{ github.token }} - run: | - changed=$(gh pr diff "${{ github.event.pull_request.number }}" \ - --repo "${{ github.repository }}" --name-only) - - views_changed=$(echo "$changed" | grep -E \ - '^feature/.*/src/commonMain/.*/(ui|component|screen)/|^feature/.*/src/androidMain/.*/ui/|^core/ui/src/commonMain/' \ - | grep -v 'Test\|Preview\|__Snapshots__' || true) - - docs_changed=$(echo "$changed" | grep -E '^docs/en/(user|developer)/' || true) - - echo "views_changed<> "$GITHUB_OUTPUT" - echo "$views_changed" >> "$GITHUB_OUTPUT" - echo "EOF" >> "$GITHUB_OUTPUT" - - echo "docs_changed<> "$GITHUB_OUTPUT" - echo "$docs_changed" >> "$GITHUB_OUTPUT" - echo "EOF" >> "$GITHUB_OUTPUT" - - if [[ -n "$views_changed" && -z "$docs_changed" ]]; then - echo "stale=true" >> "$GITHUB_OUTPUT" - else - echo "stale=false" >> "$GITHUB_OUTPUT" - fi - - - name: Post warning comment - if: steps.changed.outputs.stale == 'true' - uses: actions/github-script@v9 - with: - script: | - const viewsChanged = `${{ steps.changed.outputs.views_changed }}`.trim(); - const body = [ - '## 📄 Docs staleness check — advisory', - '', - 'This PR modifies user-facing UI source files but does not update any page under `docs/en/user/` or `docs/en/developer/`.', - '', - '> ⚠️ Doc changes propagate to **3 consumers**: in-app docs browser, Jekyll site (GitHub Pages), and meshtastic.org (Docusaurus sync). Updating a page in `docs/en/` automatically flows to all three.', - '', - '**Changed source files:**', - '```', - viewsChanged, - '```', - '', - '**What to check:**', - '| Changed area | Likely doc page |', - '|---|---|', - '| `feature/messaging/` | `docs/en/user/messages-and-channels.md` |', - '| `feature/node/` | `docs/en/user/nodes.md` or `docs/en/user/node-metrics.md` |', - '| `feature/map/` | `docs/en/user/map-and-waypoints.md` |', - '| `feature/connections/` | `docs/en/user/connections.md` |', - '| `feature/settings/` | `docs/en/user/settings-radio-user.md` or `docs/en/user/settings-module-admin.md` |', - '| `feature/firmware/` | `docs/en/user/firmware.md` |', - '| `feature/intro/` | `docs/en/user/onboarding.md` |', - '| `feature/discovery/` | `docs/en/user/discovery.md` |', - '| `feature/docs/` | Internal docs infrastructure |', - '| `core/ui/` | `docs/en/developer/codebase.md` or component-specific user pages |', - '', - '**New page checklist** (if adding a new doc page):', - '1. Create the `.md` file in `docs/en/user/` or `docs/en/developer/` with `last_updated` frontmatter', - '2. Register in `DocBundleLoader.kt` with string resources (in-app browser)', - '3. Jekyll and Docusaurus sync pick up new pages automatically — no config change needed', - '', - 'If this PR does **not** require a doc update (e.g., internal refactor, bug fix, test change), add the **`skip-docs-check`** label to dismiss this check.', - '', - '> **Cross-platform note:** This check is advisory while doc coverage matures. Both Android and Apple repos use the same `skip-docs-check` label and advisory severity. See `meshtastic/design` standards for shared conventions.', - ].join('\n'); - - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - }); - const existing = comments.find(c => - c.user.login === 'github-actions[bot]' && - c.body.includes('Docs staleness check') - ); - if (existing) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existing.id, - body, - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body, - }); - } - - - name: Dismiss stale comment when docs are updated - if: steps.changed.outputs.stale == 'false' - uses: actions/github-script@v9 - with: - script: | - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - }); - const existing = comments.find(c => - c.user.login === 'github-actions[bot]' && - c.body.includes('Docs staleness check') - ); - if (existing) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existing.id, - body: '## ✅ Docs staleness check passed\n\nThis PR includes updates to `docs/en/` alongside the source changes. Thank you!', - }); - } - - - name: Advisory status - if: steps.changed.outputs.stale == 'true' - run: | - echo "::warning::UI source files changed without corresponding docs/en/ updates." - echo "Add the 'skip-docs-check' label if this PR does not require a doc update." - echo "NOTE: This check is advisory while docs coverage matures across platforms." - echo "To upgrade to blocking, change this step to 'exit 1'." - - # ── Job 2: Quality gates — link validation, coverage, registry, freshness ─── - validate: - name: Docs quality gates - runs-on: ubuntu-24.04-arm - # Runs on pull_request (fork context, read-only token, no secrets) — never on - # pull_request_target — because it checks out and executes fork-supplied code - # (node scripts/*.js). The pull_request event makes that execution safe; the - # pull_request_target instance is skipped to avoid the pwn-request RCE and the - # actions/checkout@v7 fork-checkout refusal. - if: github.event_name != 'pull_request_target' - - steps: - - name: Checkout - uses: actions/checkout@v7.0.0 - with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} - fetch-depth: 1 - - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: "24" - - - name: Validate internal links - run: node scripts/validate-doc-links.js docs/en - - - name: Check doc coverage - run: node scripts/check-doc-coverage.js . - - - name: Validate DocBundleLoader registry - run: | - loader="feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/data/DocBundleLoader.kt" - missing=0 - for f in docs/en/user/*.md docs/en/developer/*.md; do - slug=$(basename "$f" .md) - if ! grep -q "\"$slug\"" "$loader"; then - echo "ERROR: $slug not registered in DocBundleLoader.kt" - missing=$((missing + 1)) - fi - done - if [ "$missing" -gt 0 ]; then - echo "" - echo "FAILED: $missing page(s) missing from DocBundleLoader.kt in-app index." - exit 1 - fi - echo "All doc pages registered in DocBundleLoader." - - - name: Check doc freshness - # Advisory — warns on stale pages but does not block PRs - continue-on-error: true - run: node scripts/check-doc-freshness.js docs --max-age-days=180 - - # ── Job 3: Preview staleness — flag UI changes without preview updates ────── - preview-staleness: - name: Preview staleness check - runs-on: ubuntu-24.04-arm - # pull_request_target only: needs the write token to post advisory comments. - # Reads the changed-file list via the API — no fork checkout, no fork code run. - if: >- - github.event_name == 'pull_request_target' - && github.event.pull_request != null - && !contains(github.event.pull_request.labels.*.name, 'skip-preview-check') - - steps: - - name: Detect changed files - id: changed - env: - GH_TOKEN: ${{ github.token }} - run: | - changed=$(gh pr diff "${{ github.event.pull_request.number }}" \ - --repo "${{ github.repository }}" --name-only) - - # UI composables changed (screens, components — excluding tests and previews) - ui_changed=$(echo "$changed" | grep -E \ - '^(feature|core/ui)/.*/src/commonMain/.*/.*\.(kt)$' \ - | grep -E '/(ui|component|screen)/' \ - | grep -v 'Test\|Preview\|__Snapshots__' || true) - - # Preview files changed - preview_changed=$(echo "$changed" | grep -E 'Preview.*\.kt$' || true) - - # Screenshot test files changed (regression gate + generate-only docs module) - screenshot_tests_changed=$(echo "$changed" | grep -E '^(screenshot-tests|docs-screenshots)/src/screenshotTest/.*\.kt$' || true) - - # Reference images changed (either module) - refs_changed=$(echo "$changed" | grep -E '^(screenshot-tests|docs-screenshots)/src/screenshotTestDebug/reference/.*\.png$' || true) - - echo "ui_changed<> "$GITHUB_OUTPUT" - echo "$ui_changed" >> "$GITHUB_OUTPUT" - echo "EOF" >> "$GITHUB_OUTPUT" - - echo "preview_changed<> "$GITHUB_OUTPUT" - echo "$preview_changed" >> "$GITHUB_OUTPUT" - echo "EOF" >> "$GITHUB_OUTPUT" - - echo "screenshot_tests_changed<> "$GITHUB_OUTPUT" - echo "$screenshot_tests_changed" >> "$GITHUB_OUTPUT" - echo "EOF" >> "$GITHUB_OUTPUT" - - echo "refs_changed<> "$GITHUB_OUTPUT" - echo "$refs_changed" >> "$GITHUB_OUTPUT" - echo "EOF" >> "$GITHUB_OUTPUT" - - # Preview staleness: UI changed but no preview updates - if [[ -n "$ui_changed" && -z "$preview_changed" ]]; then - echo "preview_stale=true" >> "$GITHUB_OUTPUT" - else - echo "preview_stale=false" >> "$GITHUB_OUTPUT" - fi - - # Screenshot staleness: previews changed but no reference image updates - if [[ -n "$preview_changed" && -z "$refs_changed" ]]; then - echo "screenshot_stale=true" >> "$GITHUB_OUTPUT" - else - echo "screenshot_stale=false" >> "$GITHUB_OUTPUT" - fi - - - name: Post preview advisory - if: steps.changed.outputs.preview_stale == 'true' - uses: actions/github-script@v9 - with: - script: | - const uiChanged = `${{ steps.changed.outputs.ui_changed }}`.trim(); - const body = [ - '## 🖼️ Preview staleness check — advisory', - '', - 'This PR modifies UI composables but does not update any `*Previews.kt` files.', - '', - '> Previews power screenshot tests and in-app docs screenshots. Keeping them current ensures visual regression coverage stays accurate.', - '', - '**Changed UI files:**', - '```', - uiChanged, - '```', - '', - '**What to check:**', - '| Pattern | Preview file convention |', - '|---|---|', - '| `feature/{name}/…/ui/` or `component/` | `feature/{name}/…/*Previews.kt` |', - '| `core/ui/…/` | `core/ui/…/` (previews colocated) |', - '', - '**Adding previews checklist:**', - '1. Create or update a `*Previews.kt` file in the same module with `@PreviewLightDark`', - '2. Baseline the public preview in the module `detekt-baseline.xml` (or `@Suppress("PreviewPublic")`) if it is consumed cross-module by a screenshot wrapper', - '3. Add a `@PreviewTest` wrapper: in `screenshot-tests/src/screenshotTest/` for a regression-gated component, or `docs-screenshots/src/screenshotTest/` for a doc-framed composition', - '4. Run `./gradlew :screenshot-tests:updateDebugScreenshotTest` (and `:docs-screenshots:updateDebugScreenshotTest` for doc compositions) to generate reference images', - '', - 'If this PR does **not** require preview updates (e.g., logic-only change, non-visual refactor), add the **`skip-preview-check`** label to dismiss.', - ].join('\n'); - - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - }); - const existing = comments.find(c => - c.user.login === 'github-actions[bot]' && - c.body.includes('Preview staleness check') - ); - if (existing) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existing.id, - body, - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body, - }); - } - - - name: Post screenshot advisory - if: steps.changed.outputs.screenshot_stale == 'true' - uses: actions/github-script@v9 - with: - script: | - const previewChanged = `${{ steps.changed.outputs.preview_changed }}`.trim(); - const body = [ - '## 📸 Screenshot reference staleness — advisory', - '', - 'This PR modifies preview composables but does not update screenshot reference images.', - '', - '> Reference images in `screenshot-tests/src/screenshotTestDebug/reference/` must be regenerated when previews change, or `validateDebugScreenshotTest` will fail.', - '', - '**Changed preview files:**', - '```', - previewChanged, - '```', - '', - '**How to update:**', - '```bash', - './gradlew :screenshot-tests:updateDebugScreenshotTest # regression goldens', - './gradlew :docs-screenshots:updateDebugScreenshotTest # doc-framed compositions', - './gradlew :screenshot-tests:copyDocsScreenshots # refresh docs/assets from both', - '```', - 'Then commit the updated reference PNGs (and any refreshed `docs/assets/screenshots/*.png`).', - '', - 'If this change is intentionally preview-only (e.g., adding a preview that doesn\'t need a test yet), add the **`skip-preview-check`** label.', - ].join('\n'); - - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - }); - const existing = comments.find(c => - c.user.login === 'github-actions[bot]' && - c.body.includes('Screenshot reference staleness') - ); - if (existing) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existing.id, - body, - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body, - }); - } - - - name: Dismiss comments when resolved - if: >- - steps.changed.outputs.preview_stale == 'false' - && steps.changed.outputs.screenshot_stale == 'false' - uses: actions/github-script@v9 - with: - script: | - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - }); - for (const marker of ['Preview staleness check', 'Screenshot reference staleness']) { - const existing = comments.find(c => - c.user.login === 'github-actions[bot]' && - c.body.includes(marker) - ); - if (existing) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existing.id, - body: `## ✅ ${marker} passed\n\nPreview and screenshot references are up to date.`, - }); - } - } diff --git a/.github/workflows/models_issue_triage.yml b/.github/workflows/models_issue_triage.yml deleted file mode 100644 index a16b5824d..000000000 --- a/.github/workflows/models_issue_triage.yml +++ /dev/null @@ -1,203 +0,0 @@ -name: Issue Triage (Models) - -on: - workflow_dispatch: - -permissions: - issues: write - models: read - -concurrency: - group: ${{ github.workflow }}-${{ github.event.issue.number }} - cancel-in-progress: true - -jobs: - triage: - if: ${{ github.repository == 'meshtastic/Meshtastic-Android' && github.event.issue.user.type != 'Bot' }} - runs-on: ubuntu-24.04-arm - 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@v9 - 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@v9 - 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 the Meshtastic Android app and determine if it needs labels. - - If this looks like a bug in the Android app (crash, ANR, UI glitch, connection failure, Bluetooth issues, notification problems, map issues), request app logs and explain how to get them: - - Android app debug logs: - - Open the Meshtastic app, go to Settings > Debug > Save Logs - - Reproduce the problem, then share/attach the exported log file - - Android logcat (if app logs are insufficient): - - Connect phone via USB with USB debugging enabled - - Run: adb logcat -s Meshtastic:* *:E - - Reproduce the problem, then copy/paste the relevant output - - Also request key context if missing: Android version, phone model, app version, Meshtastic device model, firmware version, connection type (BLE/USB/TCP), 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 an app 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@v9 - 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@v9 - 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@v9 - 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_pr_triage.yml b/.github/workflows/models_pr_triage.yml deleted file mode 100644 index 386327748..000000000 --- a/.github/workflows/models_pr_triage.yml +++ /dev/null @@ -1,159 +0,0 @@ -name: PR Triage (Models) - -on: - workflow_dispatch: - -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/Meshtastic-Android' && github.event.pull_request.user.type != 'Bot' }} - runs-on: ubuntu-24.04-arm - steps: - # ───────────────────────────────────────────────────────────────────────── - # Step 1: Check if PR already has automation/type labels (skip if so) - # ───────────────────────────────────────────────────────────────────────── - - name: Check existing labels - uses: actions/github-script@v9 - id: check-labels - with: - script: | - const skipLabels = new Set(['automation', 'release']); - const typeLabels = new Set(['bugfix', 'enhancement', 'dependencies', 'repo', 'refactor', 'chore', 'ci', 'build', 'testing', 'documentation']); - 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 - env: - PR_TITLE: ${{ github.event.pull_request.title }} - PR_BODY: ${{ github.event.pull_request.body }} - with: - max-tokens: 20 - prompt: | - Is this GitHub pull request spam, AI-generated slop, or low quality? - - Title: ${{ env.PR_TITLE }} - Body: ${{ env.PR_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@v9 - 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/enhancement/refactor) - # ───────────────────────────────────────────────────────────────────────── - - 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 - env: - PR_TITLE: ${{ github.event.pull_request.title }} - PR_BODY: ${{ github.event.pull_request.body }} - with: - max-tokens: 30 - prompt: | - Classify this pull request for the Meshtastic Android app into exactly one category. - - Return exactly one of: bugfix, enhancement, refactor, chore, ci, build, testing, documentation - - Label definitions: - - bugfix: Fixes a bug, crash, or incorrect behavior - - enhancement: Adds a new feature, improves UX, adds new functionality, or improves performance - - refactor: Restructures code without changing behavior, cleans up code, or improves architecture - - chore: Routine maintenance (dependency updates, config tweaks, version bumps) that doesn't change app behavior - - ci: Changes to CI/CD workflows, GitHub Actions, or automation scripts - - build: Changes to build system, Gradle config, build-logic, or compilation settings - - testing: Adds or modifies tests without changing production code - - documentation: Documentation-only changes (README, CHANGELOG, code comments, KDoc) - - Title: ${{ env.PR_TITLE }} - Body: ${{ env.PR_BODY }} - system-prompt: > - You classify pull requests into categories for changelog generation. - Pick the single most specific label. Prefer ci/build/chore/testing/documentation - over refactor when the change clearly fits those narrower categories. - Only use bugfix for actual bug fixes, not for CI fixes or build fixes. - Only use enhancement for user-facing features, not for internal improvements. - 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@v9 - 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' }, - 'enhancement': { color: 'a2eeef', description: 'New feature or enhancement' }, - 'refactor': { color: 'c5def5', description: 'Code restructuring without behavior change' }, - 'chore': { color: 'ededed', description: 'Routine maintenance' }, - 'ci': { color: 'bfd4f2', description: 'CI/CD and automation changes' }, - 'build': { color: 'bfd4f2', description: 'Build system changes' }, - 'testing': { color: 'f9d0c4', description: 'Test additions or modifications' }, - 'documentation': { color: '0075ca', description: 'Documentation changes' }, - }; - 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/moderate.yml b/.github/workflows/moderate.yml deleted file mode 100644 index b867eeddc..000000000 --- a/.github/workflows/moderate.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: AI Moderator -on: - workflow_dispatch: - -jobs: - spam-detection: - if: github.repository == 'meshtastic/Meshtastic-Android' - runs-on: ubuntu-24.04-arm - permissions: - issues: write - pull-requests: write - models: read - contents: read - steps: - - uses: actions/checkout@v7.0.0 - - uses: github/ai-moderator@v1 - with: - token: ${{ secrets.GITHUB_TOKEN }} - spam-label: 'spam' - ai-label: 'ai-generated' - minimize-detected-comments: true - # Built-in prompt configuration (all enabled by default) - enable-spam-detection: true - enable-link-spam-detection: true - enable-ai-detection: true - # custom-prompt-path: '.github/prompts/my-custom.prompt.yml' # Optional diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml index 84c75807e..307ff19ec 100644 --- a/.github/workflows/update-changelog.yml +++ b/.github/workflows/update-changelog.yml @@ -82,7 +82,9 @@ jobs: CLOSED_TAG: ${{ steps.tags.outputs.closed }} OPEN_TAG: ${{ steps.tags.outputs.open }} run: | - # Helper: generate notes using GitHub API (only works when previous_tag is a release tag) + # Helper: generate categorized notes via GitHub API, honoring + # .github/release.yml categories + excludes. Works with ANY existing + # tag as previous_tag (release OR channel), target = a tag or a SHA. generate_notes_api() { local target_sha="$1" previous_tag="$2" local body @@ -101,17 +103,6 @@ jobs: | sed -e :a -e '/^\n*$/{$d;N;ba}' } - # Helper: generate notes using GitHub compare API (works between any two refs) - generate_notes_git() { - local from_ref="$1" to_ref="$2" - gh api "repos/${REPO}/compare/${from_ref}...${to_ref}" \ - --jq '.commits[] - | select(.parents | length == 1) - | select((.commit.message | split("\n")[0] | ascii_downcase | test("update changelog(\\.md)?")) | not) - | "* \(.commit.message | split("\n")[0]) by \(.commit.author.name) (@\(.author.login // .commit.author.name)) in [`\(.sha[0:9])`](https://github.com/'"${REPO}"'/commit/\(.sha))"' \ - 2>/dev/null || true - } - # Helper: demote ### headings to #### for nesting under channel sections demote_headings() { sed 's/^### /#### /g' @@ -172,19 +163,13 @@ jobs: # === Generate segments (oldest to newest, each relative to its predecessor) === SECTIONS="" PREV_REF="$PROD_TAG" - PREV_IS_PROD=true for ((i=0; i newest tag) === @@ -219,12 +203,12 @@ jobs: LATEST_TAG="$PROD_TAG" fi if [ "$HEAD_SHA" != "$(git rev-parse "$LATEST_TAG" 2>/dev/null)" ]; then - UNRELEASED_COMMITS=$(generate_notes_git "$LATEST_TAG" "$HEAD_SHA") - if [ -n "$UNRELEASED_COMMITS" ]; then + UNRELEASED_NOTES=$(generate_notes_api "$HEAD_SHA" "$LATEST_TAG" | demote_headings) + if [ -n "$UNRELEASED_NOTES" ]; then UNRELEASED_SECTION=$(cat <