ci(check): lint build-logic in lint-check and sync the CI docs with main (#7394)

This commit is contained in:
James Rich authored and GitHub committed 2026-09-27 01:40:43 +00:00
1 parent 68df92332f
commit bc57157c19
8 files changed
+54 -24

No files matched your search

+11 -7
View File
@@ -2,7 +2,7 @@
#
# PostToolUse hook (Edit|Write|MultiEdit) for Meshtastic-Android.
#
# Front-runs three of this repo's own CI/governance gates locally, so the
# Front-runs four of this repo's own CI/governance gates locally, so the
# failure surfaces at edit time instead of in CI. Dispatches by edited path:
#
# - base strings.xml -> run scripts/sort-strings.py (keeps the file sorted
@@ -11,12 +11,17 @@
# - fastlane/metadata/** -> run scripts/check-store-metadata.py and BLOCK on
# store-rule violations (the pull-request.yml
# check-metadata job is blocking; F-Droid #4262)
# - settings.gradle.kts -> remind about the pull-request.yml paths-filter drift
# guard for NEW top-level modules (#5735)
# - settings.gradle.kts -> remind about the pull-request.yml paths-filter and
# ALL_MODULES_FULL drift guards for NEW top-level modules
# - commonMain/commonTest -> BLOCK on java.*/android.* imports in .kt files (the
# KMP boundary, otherwise first caught by the iOS compile
# in kmpSmokeCompile for main sources or allTests for tests)
#
# Kotlin edits outside tests and previews also get warn-only Compose-pitfall notes.
#
# FAILS OPEN: any tooling/parse error allows the edit to stand (exit 0). Notes are
# surfaced to Claude via PostToolUse additionalContext; only the store metadata
# check blocks (exit 2), because that one is a hard CI gate.
# surfaced to Claude via PostToolUse additionalContext; only the store metadata and
# KMP-boundary checks block (exit 2), because each front-runs a failing CI job.
input=$(cat)
@@ -41,8 +46,7 @@ emit_context() {
case "$file_path" in
*core/resources/src/commonMain/composeResources/values/strings.xml)
out=$( (cd "$repo_root" && python3 scripts/sort-strings.py) 2>&1 )
if [ $? -eq 0 ]; then
if out=$( (cd "$repo_root" && python3 scripts/sort-strings.py) 2>&1 ); then
emit_context "Auto-ran scripts/sort-strings.py: base strings.xml re-sorted and .skills/compose-ui/strings-index.txt regenerated. Line positions changed — re-read the file before any further edits to it."
else
emit_context "Tried to auto-run scripts/sort-strings.py after your strings.xml edit but it failed (likely malformed XML in what was just written — please check):
+1 -1
View File
@@ -328,7 +328,7 @@ jobs:
gh workflow run docs-release.yml --ref "$TAG"
# The Obtainium table in README.md follows the channel releases; this refreshes it
# now rather than at the next hourly run.
# now rather than at the next scheduled run.
- name: Dispatch scheduled updates
id: sched
if: ${{ inputs.channel != 'internal' }}
+2 -1
View File
@@ -105,7 +105,8 @@ jobs:
cache_configuration_cache: 'false'
- name: Spotless, Detekt & Android Lint
run: ./gradlew spotlessCheck detekt androidApp:lintFdroidDebug androidApp:lintGoogleDebug core:barcode:lintFdroidDebug core:barcode:lintGoogleDebug -Pci=true --continue
# The root spotlessCheck and detekt do not reach the included build-logic build.
run: ./gradlew spotlessCheck detekt :build-logic:convention:spotlessCheck :build-logic:convention:detekt androidApp:lintFdroidDebug androidApp:lintGoogleDebug core:barcode:lintFdroidDebug core:barcode:lintGoogleDebug -Pci=true --continue
# ── Screenshot Test Validation ──────────────────────────────────────
screenshot-check:
+1 -1
View File
@@ -113,7 +113,7 @@ specs/
The project constitution at `.specify/memory/constitution.md` defines non-negotiable principles.
All specs, plans, and tasks are validated against it during `/speckit.analyze`.
Current constitution (v1.4.0) enforces 7 principles:
Current constitution (v1.4.1) enforces 7 principles:
1. **KMP Core** — Business logic in `commonMain` only
2. **Zero Lint Tolerance** — `spotlessCheck` + `detekt` must pass
+8 -7
View File
@@ -57,6 +57,7 @@ When testing long-lived coroutines (e.g., `Flow.collect` loops launched in `back
- If touching any KMP module, also run `kmpSmokeCompile`.
- `worker/service/background` changes: Broad tests, targeted WorkManager checks.
- `BLE/networking/core repository`: `spotlessCheck`, `detekt`, `assembleDebug`, `test allTests`.
- `build-logic/` changes: also `:build-logic:convention:spotlessCheck :build-logic:convention:detekt`, which the root `spotlessCheck detekt` do not reach.
## 3) Flavor checks
@@ -100,7 +101,7 @@ Use this whenever driving the app from a fresh install/uninstall (screenshot tes
CI is defined in `.github/workflows/reusable-check.yml` as parallel job groups. No job `needs:` another, so every one queues for a runner as soon as the run starts:
1. **`lint-check`** runs `spotlessCheck`, `detekt` and Android lint for both flavors of `:androidApp` and `:core:barcode` in a single Gradle invocation (avoids 3x cold-start overhead). It checks out full history (`fetch-depth: 0`) because spotless ratchets against `origin/main`, and it has no outputs.
1. **`lint-check`** runs `spotlessCheck`, `detekt` and Android lint for both flavors of `:androidApp` and `:core:barcode` in a single Gradle invocation (avoids 3x cold-start overhead), plus `:build-logic:convention:spotlessCheck` and `:build-logic:convention:detekt`, because the root tasks do not reach the included build. It checks out full history (`fetch-depth: 0`) because spotless ratchets against `origin/main`, and it has no outputs.
2. **`test-shards`** is a 3-shard matrix that runs unit tests in parallel. Shard membership is a load-balancing detail, not a taxonomy: heavy modules are moved between shards to even out wall time, so read the matrix rather than inferring it:
- `shard-core`: `allTests` for the remaining `core:*` KMP modules, plus `kmpSmokeCompile`.
- `shard-feature`: `allTests` for `feature:*` KMP modules **plus `:core:service`**.
@@ -111,15 +112,15 @@ CI is defined in `.github/workflows/reusable-check.yml` as parallel job groups.
4. **`build-desktop`** is a multi-OS matrix (macOS, Windows, and Linux x64 and arm64; the job's `matrix.os` carries the labels) running `:desktopApp:packageDistributionForCurrentOS :desktopApp:proguardReleaseJars`. It packages the debug build type, real installers the snapshot release can ship, and pulls in `proguardReleaseJars` only so a jmods-less packaging JDK fails here rather than at release time. On Linux it then wraps jpackage's `app-image` directory into a real AppImage via `scripts/build-appimage.sh`.
5. **`screenshot-check`** — Runs `:screenshot-tests:validateDebugScreenshotTest` (the visual-regression gate) and uploads a diff report. Note: `:docs-screenshots` is intentionally NOT validated here (generate-only).
6. **`rb-check`** — Reproducible-build verification (`scripts/verify-rb.sh`). Runs **only** in the merge queue.
7. **`verify-flatpak`** lives in its own workflow (`.github/workflows/verify-flatpak.yml`), **not** in `reusable-check.yml`, and is not called by it. Generates the Flatpak offline-build sources (`captureFlatpakSources`) and then builds the flatpak fully offline, on an x86_64 + aarch64 matrix of hosted Ubuntu runners. Since #6919 the sources are generated inside each arch's own offline build rather than committed. It is **not a required check** and never runs in the merge queue, so its triggers are scoped accordingly: a PR runs it only when it touches the flatpak tooling itself (`scripts/verify-flatpak/**`, the workflow), while the wider dependency surface it captures (`gradle/libs.versions.toml`, `desktopApp/**`, `gradle/wrapper/**`, the root build scripts) is verified on push to `main` plus a nightly cron.
7. **`verify-flatpak`** lives in its own workflow (`.github/workflows/verify-flatpak.yml`), **not** in `reusable-check.yml`, and is not called by it. Generates the Flatpak offline-build sources (`captureFlatpakSources`) and then builds the flatpak fully offline, on an x86_64 + aarch64 matrix of hosted Ubuntu runners. Since #6919 the sources are generated inside each arch's own offline build rather than committed. It is **not a required check** and never runs in the merge queue, so its triggers are scoped accordingly: a PR runs it only when it touches the flatpak tooling itself (`scripts/verify-flatpak/**`, the workflow), and a push to `main` runs it for those plus `gradle/wrapper/**`, because the offline manifest pins the Gradle distribution apart from the wrapper. The wider dependency surface it captures (`desktopApp/**`, `gradle/libs.versions.toml`, the root build scripts) is verified by the nightly cron.
8. **`protobufs-bump`** — Its own workflow (`.github/workflows/protobufs-bump.yml`), on any PR that changes the `meshtastic-protobufs` line of `gradle/libs.versions.toml`. Comment only: it previews `:schema-strings:sync` at the new pin and leaves one sticky comment from `scripts/protobufs-bump-summary.py` with the upstream compare, the merged protobufs PRs, the `.proto` delta and the settings strings that will change. Nothing is pushed to the branch. The regeneration itself is a step of `scheduled-updates.yml`: `values/schema_strings.xml` records the pin it was built from, the hourly run compares that with the catalog and runs Gradle only when they differ, and the regenerated English goes up to Crowdin in the same run and rides the scheduled PR with the translations. `RepositorySyncTest` checks the file against the registry only while the recorded pin matches the catalog, so the hour between a bump merging and the scheduled PR is not red.
8. **`protobufs-bump`** runs in its own workflow (`.github/workflows/protobufs-bump.yml`) on any PR that changes the `meshtastic-protobufs` line of `gradle/libs.versions.toml`. Comment only: it previews `:schema-strings:sync` at the new pin and leaves one sticky comment from `scripts/protobufs-bump-summary.py` with the upstream compare, the merged protobufs PRs, the `.proto` delta and the settings strings that will change. Nothing is pushed to the branch. The regeneration itself is a step of `scheduled-updates.yml`, which runs every 6 hours at :17 and on every push to `main` that changes `gradle/libs.versions.toml`. `values/schema_strings.xml` records the pin it was built from; each run compares that with the catalog and, when they differ, reuses the `scheduled-updates` branch's copy if it was already built for the catalog's pin from the same `schema-strings/` generator and `strings.xml` header, running Gradle only otherwise. The regenerated English goes up to Crowdin in the same run and rides the scheduled PR with the translations. `RepositorySyncTest` checks the file against the registry only while the recorded pin matches the catalog, so the window between a bump merging and the scheduled PR landing is not red.
### Runner Strategy (Four Tiers)
The tiers are named here and the workflows carry the label versions.
- **`ubuntu-slim`** is the cheapest tier, for API and script jobs off the required-check path: the labeler, the PR-close run canceller, changelog, the Play listing upload and release cleanup. Container-backed and starts in seconds, but **single-CPU, unprivileged, x64-only, with a hard 15-minute job cap**, so it fits `gh`/`jq`/`git`/stdlib-`python3`/`github-script` work and nothing needing `sudo`, `apt-get`, Docker, a mounted filesystem, or a long full-history clone. It is a separate, smaller pool, so the required `Check Workflow Status` gates stay off it: a gate queued there holds up a finished build.
- **The pinned Ubuntu LTS arm label** runs the lightweight jobs that break any of those `ubuntu-slim` constraints or sit on the required-check path: PR and merge-queue change detection, `check-metadata`, the status gates, release metadata, the snapshot publish and promotion. Shorter queue times than x64.
- **`ubuntu-slim`** is the cheapest tier, for API and script jobs off the required-check path: the labeler, the PR-close run canceller, changelog, the docs link check, the Play listing upload, release cleanup, and the release and promotion jobs that are only API and script work (tag resolution, the version bump, the flatpak-sources and store-screenshot release assets, the GitHub release update, and the Homebrew and Flathub bumps). Container-backed and starts in seconds, but **single-CPU, unprivileged, x64-only, with a hard 15-minute job cap**, so it fits `gh`/`jq`/`git`/stdlib-`python3`/`github-script` work and nothing needing `sudo`, `apt-get`, Docker, a mounted filesystem, or a long full-history clone. It is a separate, smaller pool, so the required `Check Workflow Status` gates stay off it: a gate queued there holds up a finished build.
- **The pinned Ubuntu LTS arm label** runs the lightweight jobs that break any of those `ubuntu-slim` constraints or sit on the required-check path: PR and merge-queue change detection, `check-metadata`, the status gates, the docs quality gate, the snapshot publish, the Play upload, promotion and rollout, and the GitHub release. Shorter queue times than x64.
- **The pinned Ubuntu LTS x64 label** runs the Gradle-heavy jobs. Every single-runner job in `reusable-check.yml` pins it (`lint-check`, `screenshot-check`, `rb-check`, `test-shards`, `android-check`), as do release builds, Dokka and docs publishing. The `build-desktop` matrix job spans several runners instead, as does `verify-flatpak` in its own workflow. Pin for reproducibility.
- **Desktop runners:** a multi-OS matrix (macOS, Windows, and Ubuntu x64 and arm64) for the `build-desktop` job, `verify-flatpak`, and release packaging. Each matrix lists its own labels, which can trail the pinned LTS labels above.
@@ -146,7 +147,7 @@ The tiers are named here and the workflows carry the label versions.
- **Explicit Gradle task paths:** Prefer `androidApp:lintFdroidDebug` over shorthand `lintDebug` in CI.
- **Pull request CI:** `.github/workflows/pull-request.yml` runs on PRs into `main` and `release/**`. Its Gradle jobs skip the `scheduled-updates` and `scheduled-baseline` head branches; the merge queue still runs everything for them.
- **Merge queue hygiene:** `merge-queue.yml`'s `check-changes` job first cancels older runs for the same PR, best effort, because GitHub does not auto-cancel destroyed merge-group runs. It then lists the entry's files from the compare API, with no checkout, and skips the heavy pipeline for docs-only entries (`docs/**`, `fastlane/**`, `obtainium/**`, `*.md`), mirrored by `main-check.yml`'s `paths-ignore`. An entry of 300 or more files, the API's listing cap, runs full CI. `rb-check` runs ONLY in the merge queue. `main-check.yml` passes `run_lint: false` because every main commit is a merge-queue-verified merge commit, so main pushes skip lint, `screenshot-check` and `rb-check`, and run the coverage shards, the debug APKs and the desktop packages for the snapshot release. Its concurrency group never cancels a started run: one run executes, one waits, and a newer push replaces only the waiting one.
- **Cache writes:** each cache has its own rule. setup-gradle's Gradle User Home cache is written by `reusable-check.yml` on `main` only (`GRADLE_CACHE_READ_ONLY`), so PRs and the merge queue only read it; outside that workflow, the Google, F-Droid and desktop release builds and `scheduled-baseline.yml` write it and every other caller reads. The Develocity remote build cache is pushed by `push` and `merge_group` builds that have the access key, never by PRs (`MeshtasticDevelocitySettingsPlugin`). The plain `actions/cache` steps in `gradle-setup` (JetBrains JDK, Kotlin/Native, Robolectric) save on a key miss from any ref.
- **Cache writes:** each cache has its own rule. setup-gradle's Gradle User Home cache is written by `reusable-check.yml` on `main` only (`GRADLE_CACHE_READ_ONLY`), so PRs and the merge queue only read it; outside that workflow, the Google, F-Droid and desktop release builds and `scheduled-baseline.yml` write it and every other caller reads. The Develocity remote build cache is pushed by `push` and `merge_group` builds and by `workflow_dispatch` runs on `main` that have the access key, never by PRs (`MeshtasticDevelocitySettingsPlugin`). `gradle-setup`'s Kotlin/Native (`~/.konan`) and Robolectric caches follow the same `cache_read_only` input: they save on a key miss only when it is not `'true'`, and a read-only run restores them without saving. Kotlin/Native is opt-in through `cache_konan`, which the test shards and the read-only docs builds pass, so only the test shards on `main` write it; `cache_robolectric` opts a job out of the Robolectric cache.
- **Path filtering:** `check-changes` in `pull-request.yml` must include module dirs plus build/workflow entrypoints (`build-logic/**`, `gradle/**`, `.github/workflows/**`, `gradlew`, `settings.gradle.kts`, etc.). It runs three drift guards, each runnable locally with `python3`: `scripts/check-changes-filter.py` (every module root in `settings.gradle.kts` has a `<root>/**` line in the `android` filter), `scripts/check-module-list.py` (`RootConventionPlugin`'s `ALL_MODULES_FULL` matches `settings.gradle.kts`) and `scripts/check-test-shards.py` (every module with tests is in a `reusable-check.yml` shard or exempted).
- **AboutLibraries:** Runs in `offlineMode` by default (no GitHub/SPDX API calls). Release builds pass `-PaboutLibraries.release=true` via Fastlane/Gradle CLI to enable remote license fetching. Do NOT re-gate on `CI` or `GITHUB_TOKEN` alone.
- **AboutLibraries:** Runs in `offlineMode` by default (no GitHub/SPDX API calls). `release.yml`'s Google and desktop build steps pass `-PaboutLibraries.release=true` to enable remote license fetching; the F-Droid build leaves it off so its output matches F-Droid's reproducible rebuild. Do NOT re-gate on `CI` or `GITHUB_TOKEN` alone.
+1 -1
View File
@@ -35,7 +35,7 @@ full refresh also appends a ~95-line managed SPECKIT GOVERNANCE section to
- Package manifest: `build.gradle.kts`, `settings.gradle.kts`, `gradle/libs.versions.toml`
(`docs/Gemfile` + `docs/Gemfile.lock` belong to the Jekyll docs site only)
- Task runners: Gradle wrapper (`gradlew`), convention plugins in `build-logic/`
- CI workflows: `.github/workflows/create-or-promote-release.yml` `.github/workflows/docs-deploy.yml`,`.github/workflows/docs-release.yml` `.github/workflows/main-check.yml`,`.github/workflows/merge-queue.yml` `.github/workflows/msstore-publish.yml`,`.github/workflows/post-release-cleanup.yml` `.github/workflows/pr-closed-cleanup.yml`,`.github/workflows/promote.yml` `.github/workflows/pull-request-target.yml`,`.github/workflows/pull-request.yml` `.github/workflows/release.yml`,`.github/workflows/reusable-check.yml` `.github/workflows/scheduled-baseline.yml`,`.github/workflows/scheduled-updates.yml` `.github/workflows/update-changelog.yml` `.github/workflows/verify-flatpak.yml`,`.github/workflows/winget-publish.yml`
- CI workflows: `.github/workflows/create-or-promote-release.yml`, `.github/workflows/docs-deploy.yml`, `.github/workflows/docs-link-check.yml`, `.github/workflows/docs-quality.yml`, `.github/workflows/docs-release.yml`, `.github/workflows/main-check.yml`, `.github/workflows/merge-queue.yml`, `.github/workflows/msstore-publish.yml`, `.github/workflows/play-listing.yml`, `.github/workflows/play-rollout.yml`, `.github/workflows/post-release-cleanup.yml`, `.github/workflows/pr-closed-cleanup.yml`, `.github/workflows/promote.yml`, `.github/workflows/protobufs-bump.yml`, `.github/workflows/pull-request-target.yml`, `.github/workflows/pull-request.yml`, `.github/workflows/release.yml`, `.github/workflows/reusable-check.yml`, `.github/workflows/scheduled-baseline.yml`, `.github/workflows/scheduled-updates.yml`, `.github/workflows/store-screenshots.yml`, `.github/workflows/update-changelog.yml`, `.github/workflows/verify-flatpak.yml`, `.github/workflows/version-bump.yml`, `.github/workflows/winget-publish.yml`
- Source paths: `androidApp/`, `desktopApp/`, `core/`, `feature/`, `build-logic/`, `scripts/`
- Test paths: `**/src/commonTest/`, `**/src/jvmTest/`, `**/src/androidHostTest/`,
`screenshot-tests/`, `docs-screenshots/`, `baselineprofile/`, `core/konsist/`
+30 -5
View File
@@ -1,4 +1,26 @@
<!--
SYNC IMPACT REPORT
==================
Version change: 1.4.0 → 1.4.1
Modified principles:
- VI. Documentation Freshness: the docs-quality.yml gate also runs on PRs that touch
any docs/**/*.md, locale pages included, and runs
scripts/docs/sync-locale-front-matter.py --check, which fails when a docs/<locale>/
page's layout or nav_order differs from docs/en. The local command list gains that
check. PATCH: the principle now describes the gate the workflow already runs.
Added sections: none
Removed sections: none
Templates requiring updates:
- .specify/templates/plan-template.md ✅ no change (names no gate trigger)
- .specify/templates/checklist-template.md ✅ no change (CHK006 names no gate trigger)
- .specify/templates/spec-template.md ✅ no reference
- .specify/templates/tasks-template.md ✅ no reference
Downstream references (Amendment Procedure step 3):
- .skills/speckit/SKILL.md ✅ updated (declared constitution version; its principle VI
summary already names the locale check)
- AGENTS.md ✅ no change (names no docs gate; principle count still 7)
Follow-up TODOs: none
SYNC IMPACT REPORT
==================
Version change: 1.3.7 → 1.4.0
@@ -198,15 +220,18 @@ Governance rules:
from Crowdin (`crowdin.yml`) — never hand-edit a locale page; deleting an English page
means deleting its locale copies in the same commit.
Verification tooling — also enforced in CI: `.github/workflows/docs-quality.yml` runs the
link check, the coverage check, a two-way `DocBundleLoader.kt` registry check, and the alias
registration check as a **blocking** gate on PRs touching `docs/en/**` (freshness stays
advisory). Run locally before pushing docs changes:
Verification tooling, also enforced in CI: `.github/workflows/docs-quality.yml` runs the
link check, the coverage check, a two-way `DocBundleLoader.kt` registry check, the alias
registration check, and a locale front matter check (`layout` and `nav_order` in every
`docs/<locale>/` page match `docs/en/`) as a **blocking** gate on PRs touching `docs/en/**`
or any other `docs/**/*.md`, locale pages included (freshness stays advisory). Run locally
before pushing docs changes:
```bash
node scripts/check-doc-coverage.js # every user-facing feature module has a page
node scripts/validate-doc-links.js # internal cross-references and image paths resolve
node scripts/check-doc-aliases.js # frontmatter aliases are registered in DocBundleLoader.kt
python3 scripts/docs/sync-locale-front-matter.py --check # locale layout/nav_order match docs/en; drop --check to restore
node scripts/check-doc-freshness.js # advisory: pages >180 days old, or missing last_updated
```
<!-- Rationale: Documentation drift misleads users and increases support burden. Three distinct consumers means changes must be verified across all delivery channels. -->
@@ -290,4 +315,4 @@ summary derived from this constitution. The files `.github/copilot-instructions.
Constitution Check confirming all seven principles were evaluated. Complexity violations
require explicit justification in the Complexity Tracking table of the plan document.
**Version**: 1.4.0 | **Ratified**: 2026-05-07 | **Last Amended**: 2026-09-15
**Version**: 1.4.1 | **Ratified**: 2026-05-07 | **Last Amended**: 2026-09-26
@@ -26,7 +26,6 @@ import org.gradle.api.provider.Provider
import org.gradle.api.tasks.testing.AbstractTestTask
import org.gradle.api.tasks.testing.Test
import org.gradle.api.tasks.testing.logging.TestLogEvent
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.getByType
import org.gradle.kotlin.dsl.withType
import org.gradle.plugin.use.PluginDependency