mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-07-03 01:45:36 -04:00
chore(ci): prune dead workflows, move vuln scanning to Renovate, fix changelog gaps (#6000)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2
.github/release.yml
vendored
2
.github/release.yml
vendored
@@ -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:
|
||||
|
||||
1
.github/renovate.json
vendored
1
.github/renovate.json
vendored
@@ -9,6 +9,7 @@
|
||||
"workarounds:all"
|
||||
],
|
||||
"commitMessageTopic": "{{depName}}",
|
||||
"osvVulnerabilityAlerts": true,
|
||||
"labels": [
|
||||
"dependencies"
|
||||
],
|
||||
|
||||
29
.github/workflows/dependency-submission.yml
vendored
29
.github/workflows/dependency-submission.yml
vendored
@@ -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"
|
||||
419
.github/workflows/docs-governance.yml
vendored
419
.github/workflows/docs-governance.yml
vendored
@@ -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<<EOF" >> "$GITHUB_OUTPUT"
|
||||
echo "$views_changed" >> "$GITHUB_OUTPUT"
|
||||
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "docs_changed<<EOF" >> "$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<<EOF" >> "$GITHUB_OUTPUT"
|
||||
echo "$ui_changed" >> "$GITHUB_OUTPUT"
|
||||
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "preview_changed<<EOF" >> "$GITHUB_OUTPUT"
|
||||
echo "$preview_changed" >> "$GITHUB_OUTPUT"
|
||||
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "screenshot_tests_changed<<EOF" >> "$GITHUB_OUTPUT"
|
||||
echo "$screenshot_tests_changed" >> "$GITHUB_OUTPUT"
|
||||
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "refs_changed<<EOF" >> "$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.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
203
.github/workflows/models_issue_triage.yml
vendored
203
.github/workflows/models_issue_triage.yml
vendored
@@ -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
|
||||
});
|
||||
159
.github/workflows/models_pr_triage.yml
vendored
159
.github/workflows/models_pr_triage.yml
vendored
@@ -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] });
|
||||
26
.github/workflows/moderate.yml
vendored
26
.github/workflows/moderate.yml
vendored
@@ -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
|
||||
30
.github/workflows/update-changelog.yml
vendored
30
.github/workflows/update-changelog.yml
vendored
@@ -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<N; i++)); do
|
||||
LABEL="${TAG_LABELS[$i]}"
|
||||
TAG="${TAG_NAMES[$i]}"
|
||||
SHA="${TAG_SHAS[$i]}"
|
||||
|
||||
# Use GitHub API when previous ref is a production tag, git log otherwise
|
||||
if [ "$PREV_IS_PROD" = true ]; then
|
||||
NOTES=$(generate_notes_api "$SHA" "$PREV_REF" | demote_headings)
|
||||
else
|
||||
NOTES=$(generate_notes_git "$PREV_REF" "$SHA")
|
||||
fi
|
||||
NOTES=$(generate_notes_api "$SHA" "$PREV_REF" | demote_headings)
|
||||
|
||||
if [ -n "$NOTES" ]; then
|
||||
SECTION=$(cat <<SECTION
|
||||
@@ -205,7 +190,6 @@ jobs:
|
||||
fi
|
||||
|
||||
PREV_REF="$TAG"
|
||||
PREV_IS_PROD=false
|
||||
done
|
||||
|
||||
# === Unreleased segment (HEAD -> 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 <<SECTION
|
||||
### Unreleased (not yet in any build)
|
||||
|
||||
${UNRELEASED_COMMITS}
|
||||
${UNRELEASED_NOTES}
|
||||
SECTION
|
||||
)
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user