Files
Meshtastic-Android/.github/workflows/scheduled-updates.yml
2026-06-18 14:45:48 -05:00

267 lines
12 KiB
YAML

name: Scheduled Updates (Firmware, Hardware, Translations)
on:
schedule:
- cron: '0 */6 * * *' # Run every 6 hours (raised from 4h to absorb the added baseline-profile step)
workflow_dispatch: # Allow manual triggering
jobs:
update_assets:
runs-on: ubuntu-24.04
if: github.repository == 'meshtastic/Meshtastic-Android'
permissions:
contents: write # To commit files and push branches
pull-requests: write # To create pull requests
steps:
- name: Checkout repository
uses: actions/checkout@v7.0.0
with:
token: ${{ secrets.CROWDIN_GITHUB_TOKEN }}
- name: Update firmware releases list
id: firmware
run: |
firmware_file_path="androidApp/src/main/assets/firmware_releases.json"
temp_firmware_file="/tmp/new_firmware_releases.json"
echo "Fetching latest firmware releases..."
http_code=$(curl -s -o "$temp_firmware_file" -w '%{http_code}' https://api.meshtastic.org/github/firmware/list || true)
http_code="${http_code:-0}"
if [ "$http_code" -lt 200 ] || [ "$http_code" -ge 300 ]; then
echo "::warning::Firmware API returned HTTP $http_code. Skipping firmware update."
echo "status=error" >> "$GITHUB_OUTPUT"
echo "detail=HTTP $http_code from firmware API" >> "$GITHUB_OUTPUT"
elif ! jq empty "$temp_firmware_file" 2>/dev/null; then
echo "::warning::Firmware API returned invalid JSON data. Skipping firmware update."
echo "status=error" >> "$GITHUB_OUTPUT"
echo "detail=Invalid JSON response from firmware API" >> "$GITHUB_OUTPUT"
else
if [ ! -f "$firmware_file_path" ] || ! jq --sort-keys . "$temp_firmware_file" | diff -q - <(jq --sort-keys . "$firmware_file_path"); then
echo "Changes detected in firmware list or local file missing. Updating $firmware_file_path."
cp "$temp_firmware_file" "$firmware_file_path"
echo "status=updated" >> "$GITHUB_OUTPUT"
else
echo "No changes detected in firmware list."
echo "status=unchanged" >> "$GITHUB_OUTPUT"
fi
fi
- name: Update hardware list
id: hardware
run: |
hardware_file_path="androidApp/src/main/assets/device_hardware.json"
temp_hardware_file="/tmp/new_device_hardware.json"
echo "Fetching latest device hardware data..."
http_code=$(curl -s -o "$temp_hardware_file" -w '%{http_code}' https://api.meshtastic.org/resource/deviceHardware || true)
http_code="${http_code:-0}"
if [ "$http_code" -lt 200 ] || [ "$http_code" -ge 300 ]; then
echo "::warning::Hardware API returned HTTP $http_code. Skipping hardware update."
echo "status=error" >> "$GITHUB_OUTPUT"
echo "detail=HTTP $http_code from hardware API" >> "$GITHUB_OUTPUT"
elif ! jq empty "$temp_hardware_file" 2>/dev/null; then
echo "::warning::Hardware API returned invalid JSON data. Skipping hardware update."
echo "status=error" >> "$GITHUB_OUTPUT"
echo "detail=Invalid JSON response from hardware API" >> "$GITHUB_OUTPUT"
else
if [ ! -f "$hardware_file_path" ] || ! jq --sort-keys . "$temp_hardware_file" | diff -q - <(jq --sort-keys . "$hardware_file_path"); then
echo "Changes detected in hardware list or local file missing. Updating $hardware_file_path."
cp "$temp_hardware_file" "$hardware_file_path"
echo "status=updated" >> "$GITHUB_OUTPUT"
else
echo "No changes detected in hardware list."
echo "status=unchanged" >> "$GITHUB_OUTPUT"
fi
fi
- name: Sync with Crowdin
uses: crowdin/github-action@v2
with:
base_url: 'https://meshtastic.crowdin.com/api/v2'
config: 'crowdin.yml'
crowdin_branch_name: 'main'
upload_sources: true
upload_sources_args: '--preserve-hierarchy'
upload_translations: false
download_translations: true
download_translations_args: '--preserve-hierarchy'
create_pull_request: false
commit_message: 'chore(l10n): New Crowdin Translations from scheduled update'
push_translations: false
push_sources: false
localization_branch_name: ${{ github.ref_name }}
env:
GITHUB_TOKEN: ${{ secrets.CROWDIN_GITHUB_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
- name: Fix file permissions
run: sudo chown -R $USER:$USER .
# Early warning for overlength store-listing translations just pulled from
# Crowdin. Non-blocking on purpose: a hard failure here would abort the
# firmware/hardware/graphs job and stop the PR from being created. The
# hard gate is the check-metadata job in pull-request.yml, which runs
# against the PR this workflow opens.
- name: Check store metadata lengths
continue-on-error: true
run: python3 scripts/check-metadata-length.py
- name: Gradle Setup
uses: ./.github/actions/gradle-setup
with:
gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
cache_read_only: 'false'
- name: Update Graphs
run: ./gradlew graphUpdate
continue-on-error: true
# ── Baseline Profile regeneration ───────────────────────────────────
# Runs on every scheduled tick (and manual dispatch). Generation needs a booted emulator
# (~10 min); continue-on-error keeps flakiness from blocking the firmware/translation PR.
- name: Enable KVM (for the emulator)
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \
| sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Generate Baseline Profile
id: generate_baseline
continue-on-error: true # Emulator flakiness must not block the firmware/translation PR.
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 34
target: google_apis # google flavor needs GMS (Maps) on the device image
arch: x86_64
profile: pixel_6
disable-animations: true
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
# Writes androidApp/src/google/generated/baselineProfiles/ via the androidx.baselineprofile plugin.
# --no-configuration-cache: the underlying connectedGoogleNonMinifiedReleaseAndroidTest task is not
# config-cache serializable (SeparateTestModuleTestData / ResolutionBackedFileCollection), and the
# project enables org.gradle.configuration-cache by default — same workaround used in reusable-check.yml.
script: ./gradlew :androidApp:generateGoogleReleaseBaselineProfile -Pci=true --no-configuration-cache
- name: Detect baseline profile changes
id: baseline
run: |
profile_dir="androidApp/src/google/generated/baselineProfiles"
outcome="${{ steps.generate_baseline.outcome }}"
if [ "$outcome" = "skipped" ]; then
echo "status=skipped" >> "$GITHUB_OUTPUT"
elif [ "$outcome" != "success" ]; then
echo "::warning::Baseline profile generation failed (outcome: $outcome). Skipping."
echo "status=error" >> "$GITHUB_OUTPUT"
elif [ -n "$(git status --porcelain "$profile_dir" 2>/dev/null)" ]; then
echo "status=updated" >> "$GITHUB_OUTPUT"
else
echo "status=unchanged" >> "$GITHUB_OUTPUT"
fi
- name: Build PR body
id: pr_body
run: |
firmware_status="${{ steps.firmware.outputs.status }}"
firmware_detail="${{ steps.firmware.outputs.detail }}"
hardware_status="${{ steps.hardware.outputs.status }}"
hardware_detail="${{ steps.hardware.outputs.detail }}"
baseline_status="${{ steps.baseline.outputs.status }}"
body="This PR includes automated updates from the scheduled workflow:"
body+=$'\n'
# Firmware status
case "$firmware_status" in
updated) body+=$'\n'"- ✅ \`firmware_releases.json\` updated from the Meshtastic API." ;;
unchanged) body+=$'\n'"- ✔️ \`firmware_releases.json\` checked — no changes detected." ;;
error) body+=$'\n'"- ⚠️ \`firmware_releases.json\` skipped — ${firmware_detail}." ;;
*) body+=$'\n'"- ❓ \`firmware_releases.json\` — unknown status." ;;
esac
# Hardware status
case "$hardware_status" in
updated) body+=$'\n'"- ✅ \`device_hardware.json\` updated from the Meshtastic API." ;;
unchanged) body+=$'\n'"- ✔️ \`device_hardware.json\` checked — no changes detected." ;;
error) body+=$'\n'"- ⚠️ \`device_hardware.json\` skipped — ${hardware_detail}." ;;
*) body+=$'\n'"- ❓ \`device_hardware.json\` — unknown status." ;;
esac
# Baseline profile (daily / manual only)
case "$baseline_status" in
updated) body+=$'\n'"- ✅ \`androidApp\` baseline profile regenerated on an emulator." ;;
unchanged) body+=$'\n'"- ✔️ \`androidApp\` baseline profile regenerated — no changes detected." ;;
error) body+=$'\n'"- ⚠️ \`androidApp\` baseline profile generation failed — skipped (see workflow logs)." ;;
skipped) ;; # Not a daily/manual run — omit the line entirely.
*) ;;
esac
# Crowdin & graphs (always attempted)
body+=$'\n'"- Source strings were uploaded to Crowdin."
body+=$'\n'"- Latest translations were downloaded from Crowdin (if available)."
body+=$'\n'"- Updated module dependency graphs in README.md files (if changed)."
body+=$'\n'
body+=$'\n'"Please review the changes."
# Write multi-line body to output
{
echo "content<<PREOF"
echo "$body"
echo "PREOF"
} >> "$GITHUB_OUTPUT"
- name: Create Pull Request if changes occurred
uses: peter-evans/create-pull-request@v8
with:
token: ${{ secrets.CROWDIN_GITHUB_TOKEN }}
commit-message: |
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs, Baseline)
Automated updates for:
- Firmware releases list
- Device hardware list
- Crowdin source string uploads
- Crowdin translation downloads
- Module dependency graphs
- androidApp baseline profile
title: 'chore: Scheduled updates (Firmware, Hardware, Translations, Graphs, Baseline)'
body: ${{ steps.pr_body.outputs.content }}
branch: 'scheduled-updates'
base: 'main'
delete-branch: true
# Only stage the baseline-profile dir when generation actually produced changes. When the emulator
# boots far enough for Gradle to create an *empty* baselineProfiles/ dir but generation then fails,
# `git add <glob>` aborts with "pathspec did not match any files", which would hard-fail this step
# and defeat the continue-on-error on the generation step above.
add-paths: |
androidApp/src/main/assets/firmware_releases.json
androidApp/src/main/assets/device_hardware.json
${{ steps.baseline.outputs.status == 'updated' && 'androidApp/src/google/generated/baselineProfiles/**' || '' }}
fastlane/metadata/android/**
**/strings.xml
**/README.md
docs/**/*.md
labels: |
automation
l10n
firmware
hardware
check-workflow-status:
name: Check Workflow Status
runs-on: ubuntu-24.04-arm
permissions: {}
needs:
- update_assets
if: always()
steps:
- name: Check Workflow Status
if: "contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')"
run: |
echo "One of the dependent jobs failed or was cancelled. Failing the workflow."
exit 1