Files
Meshtastic-Android/.github/workflows/pull-request.yml
T

274 lines
15 KiB
YAML

name: Pull Request CI
on:
pull_request:
branches: [ main, "release/**" ]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
# 1. CHANGE DETECTION: Prevents unnecessary builds. Also verifies the path
# filter below stays aligned with the module roots in settings.gradle.kts
# (folded into this job rather than run standalone: runner-pool slots, not
# compute, are the scarce resource during queue bursts).
check-changes:
# scheduled-baseline changes only the googleRelease baseline profile and READMEs, which
# no PR job builds; the merge queue still runs everything.
if: github.repository == 'meshtastic/Meshtastic-Android' && !( github.head_ref == 'scheduled-updates' || github.head_ref == 'scheduled-baseline' )
runs-on: ubuntu-26.04-arm
timeout-minutes: 10
outputs:
android: ${{ steps.filter.outputs.android }}
screenshots: ${{ steps.filter.outputs.screenshots }}
desktop: ${{ steps.filter.outputs.desktop }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4
id: filter
with:
token: ''
filters: |
# Anything in screenshot-tests' dependency closure (feature/* and
# core/* transitively) or the build machinery that shapes rendering.
# androidApp-, desktopApp- and docs-only changes skip screenshots on
# PRs; the merge queue always runs them as the final gate.
screenshots:
- 'screenshot-tests/**'
- 'core/**'
- 'feature/**'
- 'build-logic/**'
- 'config/**'
- 'gradle/**'
- 'build.gradle.kts'
- 'settings.gradle.kts'
- 'gradle.properties'
- 'config.properties'
- 'compose_compiler_config.conf'
- '.github/workflows/**'
- '.github/actions/**'
# Desktop packaging only runs post-merge on main, so windows-latest never saw a
# PR — that is how #6777's Gradle 9.7.1 bump went green here and reddened main
# (CMP's MSI/WiX path reads Project.layout on the root project from ':desktopApp',
# and only Windows trips it). Narrow gate rather than always-on: the 4-OS matrix is
# ~40 runner-minutes, and macOS bills at 10x.
# libs.versions.toml is in deliberately, whole-file: #6904 was a one-line CMP bump
# in the catalog, and CMP's packaging is exactly what breaks. Same over-run trade
# verify-flatpak already accepts (#6911) — a filter naming today's keys goes stale
# silently the moment the build reads another one.
desktop:
- 'desktopApp/**'
- 'scripts/build-appimage.sh'
# Shared build machinery that shapes the packaged output.
- 'build-logic/**'
- 'gradle/wrapper/**'
- 'gradle/libs.versions.toml'
# Root inputs the packaging tasks actually read: config.properties supplies the
# version metadata baked into the installers (ProjectExtensions.kt, VersionInfo.kt),
# and the rest change plugin resolution, configuration, or how gradlew is invoked.
- 'build.gradle.kts'
- 'settings.gradle.kts'
- 'gradle.properties'
- 'config.properties'
- 'gradlew'
- 'gradlew.bat'
- '.github/workflows/reusable-check.yml'
- '.github/actions/gradle-setup/**'
android:
# CI/workflow implementation
- '.github/workflows/**'
- '.github/actions/**'
# Product modules validated by reusable-check
- 'androidApp/**'
- 'baselineprofile/**'
- 'desktopApp/**'
- 'core/**'
- 'feature/**'
- 'screenshot-tests/**'
- 'docs-screenshots/**'
- 'store-screenshots/**'
- 'schema-strings/**'
# Shared build infrastructure
- 'build-logic/**'
- 'config/**'
- 'gradle/**'
# Root build entrypoints/config that can alter task graph or outputs
- 'build.gradle.kts'
- 'config.properties'
- 'compose_compiler_config.conf'
- 'gradle.properties'
- 'gradlew'
- 'gradlew.bat'
- 'settings.gradle.kts'
- 'test.gradle.kts'
- name: Verify module roots are represented in check-changes filter
run: python3 scripts/check-changes-filter.py
- name: Verify the root module list matches settings.gradle.kts
run: python3 scripts/check-module-list.py
- name: Verify every module with tests is wired into a CI test shard
run: python3 scripts/check-test-shards.py
# 1c. REPO CHECKS: actionlint, shellcheck, the script self-tests, and the store-listing,
# AppStream and generated-file checks. Store listings are mirrored from Crowdin, so
# this job intentionally runs on the translation-sync PRs too (no
# scheduled-updates skip) -- that is where overlength translations
# land. It is a standalone lightweight job, decoupled from the Gradle build so
# a one-line translation fix never triggers a full assemble/test cycle.
check-metadata:
name: Check Workflows, Scripts & Metadata
if: github.repository == 'meshtastic/Meshtastic-Android'
runs-on: ubuntu-26.04-arm
timeout-minutes: 5
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# actionlint plus its shellcheck pass over every run: block. The image bundles
# shellcheck and reads .github/actionlint.yaml from the mounted workspace.
- name: Lint GitHub workflows (actionlint)
uses: docker://rhysd/actionlint:1.7.12@sha256:b1934ee5f1c509618f2508e6eb47ee0d3520686341fec936f3b79331f9315667
with:
args: -color
- name: Lint repo shell scripts (shellcheck)
# -x so sourced libraries are followed into rather than reported as SC1091, and
# find rather than a glob: scripts/*.sh missed scripts/lib/, scripts/docs/ and
# scripts/verify-flatpak/ entirely.
run: find scripts -name '*.sh' -print0 | xargs -0 shellcheck -x
# rb-check runs only in the merge queue and needs two release builds, so Step 6's
# classification would otherwise ship unexercised. It lives here rather than in
# lint-check because it needs no toolchain, and because lint-check sits behind the
# `android` path filter — which does not list scripts/, so a PR touching only the
# scanner would have skipped its own test.
- name: Self-test verify-rb Step 6
run: ./scripts/verify-rb-selftest.sh
- name: Self-test verify-abi-parity
run: ./scripts/verify-abi-parity-selftest.sh
# Locale codes, lengths and HTML for all three stores that read the tree. supply
# uploads every locale directory it finds and neither it nor the Crowdin CLI
# validates the names, so an unmapped code is only rejected by the Play API, partway
# through an upload; F-Droid and IzzyOnDroid read the tree straight from git.
- name: Validate store listing metadata
run: python3 scripts/check-store-metadata.py
# default.txt is rendered from metainfo.xml; a PR that edits one without the
# other ships a listing that no longer says what the release did.
- name: Check Play what's-new matches AppStream
run: python3 scripts/sync-play-changelog.py --check
# Compose Multiplatform does not strip Android-style \" / \' escapes, so a
# backslash written before a quote renders literally in the UI (PR #6357).
# Guards the English source strings; locale mirrors are cleaned upstream by
# the Crowdin post-export processor.
- name: Check for escaped quotes in base string resources
run: python3 scripts/check-string-escapes.py
# The deep-link tables (README + developer guide) and both Obtainium import
# files are generated from CHANNELS x FLAVORS in obtainium/generate-links.py.
# Offline and deterministic, so drift can only come from a commit: this fails
# the PR that hand-edits a generated file or changes the script without
# regenerating. The network half (are the APK filters still matching real
# release assets?) is the --refresh probe in scheduled-updates.yml.
- name: Check Obtainium generated links are current
run: python3 obtainium/generate-links.py --check
# The flatpak offline manifest is generated on x86_64 but consumed by an arm64 builder, so the
# root build.gradle.kts has to force-resolve every artifact the two arches resolve differently.
# Since flatpak-sources 0.2.0 those resolve transitively, so the natives look after themselves —
# but a *root* does not: a per-architecture runtime desktopApp resolves that nothing declares has
# nothing to expand from, and the miss surfaces as `Could not find <jar>` eleven minutes into the
# arm64 build (#6901). Offline, so it costs seconds. It lives here rather than in
# verify-flatpak.yml because that workflow's path filter excludes gradle/libs.versions.toml — a
# dependency bump, the very thing that causes this drift, would never have run it.
- name: Check flatpak platform dependencies cover every per-arch runtime
run: python3 scripts/verify-flatpak/check-platform-deps.py
# Flathub/AppStream needs a <release> entry for the version being shipped; a stale
# <releases> block degrades (or fails) the Flathub listing. Fails the PR that bumps
# VERSION_NAME_BASE until the matching entry is added.
- name: Require AppStream release entry for current version
run: |
VERSION=$(grep '^VERSION_NAME_BASE=' config.properties | cut -d'=' -f2)
METAINFO=desktopApp/packaging/linux/org.meshtastic.MeshtasticDesktop.metainfo.xml
if ! grep -q "version=\"$VERSION\"" "$METAINFO"; then
echo "::error file=$METAINFO::Missing <release version=\"$VERSION\"> entry. Add it alongside the VERSION_NAME_BASE bump."
exit 1
fi
# Flathub wants screenshot links from a tag or a commit, never a branch. Ours
# are the release assets the internal cut attaches, so they name the version.
if grep '<image>' "$METAINFO" | grep -qvF "/releases/download/v$VERSION/"; then
echo "::error file=$METAINFO::Every <image> URL must point at the v$VERSION release assets (releases/download/v$VERSION/...). Update them alongside the VERSION_NAME_BASE bump."
exit 1
fi
# Flathub runs this exact command (flatpak-builder-lint checks/metainfo.py) and turns any
# non-zero exit into appstream-failed-validation at publish time. Nothing here ran it, so a
# malformed <description> reached Flathub before CI ever saw it. Catches a whitespace-only or
# empty-<p> description; a truly empty <description></description> and a release with no
# description at all both still validate, so this is hygiene, not a prose gate.
- name: Validate AppStream metainfo
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq appstream
appstreamcli validate --no-net \
desktopApp/packaging/linux/org.meshtastic.MeshtasticDesktop.metainfo.xml
# 2. VALIDATION & BUILD: Delegate to reusable-check.yml
# Coverage stays off for PRs to keep feedback fast (< 10 mins); mainline coverage comes
# from main-check. Desktop *compilation* is covered on every PR by :desktopApp:test in the
# shard-app shard, so the desktop matrix here is about packaging — the jpackage/WiX path
# that compilation never reaches — and only runs when the desktop filter matches.
validate-and-build:
needs: check-changes
# `desktop` as well as `android`: scripts/build-appimage.sh is in the desktop filter but
# deliberately not the android one, so gating on android alone would skip this whole
# workflow for an AppImage-only change and the desktop matrix would never run.
if: needs.check-changes.outputs.android == 'true' || needs.check-changes.outputs.desktop == 'true'
uses: ./.github/workflows/reusable-check.yml
permissions:
contents: read
pull-requests: write # Gradle job summary as a PR comment on failure
with:
run_lint: true
run_screenshot_tests: ${{ needs.check-changes.outputs.screenshots == 'true' }}
run_unit_tests: true
run_coverage: false
# Installers (dmg/msi+exe/deb+rpm+AppImage) upload on every run that builds them —
# upload_artifacts is already true here — so reviewers can install a PR's desktop build
# instead of waiting for the post-merge snapshot.
run_desktop_builds: ${{ needs.check-changes.outputs.desktop == 'true' }}
upload_artifacts: true
secrets: inherit
# 3. WORKFLOW STATUS: Ensures required checks are satisfied
# Pure gate job: no checkout, no toolchain, just reads `needs` results. It is the required
# check, so it runs on a hosted Ubuntu label that shares the org's pool with the build jobs,
# not on ubuntu-slim's separate pool: an aggregator queued behind slim blocks a finished build.
check-workflow-status:
name: Check Workflow Status
runs-on: ubuntu-26.04-arm
timeout-minutes: 5
permissions: {}
needs: [check-changes, check-metadata, validate-and-build]
if: always()
steps:
- name: Check Workflow Status
run: |
# skipped is fine (bot branches); failure also covers the filter-drift step
if [[ "${{ needs.check-changes.result }}" == "failure" || "${{ needs.check-changes.result }}" == "cancelled" ]]; then
echo "::error::Change detection or filter drift check failed"
exit 1
fi
if [[ "${{ needs.check-metadata.result }}" == "failure" || "${{ needs.check-metadata.result }}" == "cancelled" ]]; then
echo "::error::Workflow, script or metadata checks failed"
exit 1
fi
# skipped means neither the android nor the desktop filter matched
if [[ "${{ needs.validate-and-build.result }}" == "failure" || "${{ needs.validate-and-build.result }}" == "cancelled" ]]; then
echo "::error::Android Check failed"
exit 1
fi
# If no changes were detected, this still succeeds to satisfy required status check
echo "Workflow status satisfied."