mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-10-03 00:54:34 -04:00
ci(release): upload to Play after every leg builds and automate the promotion tail (#7390)
This commit is contained in:
1 parent
b1e696b841
commit
4561b100fb
19 files changed
+916
-573
No files matched your search
@@ -0,0 +1,78 @@
|
||||
name: Bot PR
|
||||
description: Open or update a PR from the working tree's changes and hand it to the merge queue.
|
||||
# The token must be a PAT: GITHUB_TOKEN may not enable auto-merge on protected main, and
|
||||
# PRs it opens start no workflows, so the required checks would never report.
|
||||
inputs:
|
||||
token:
|
||||
description: 'PAT that pushes the branch, opens the PR and requests the merge'
|
||||
required: true
|
||||
branch:
|
||||
description: 'PR head branch'
|
||||
required: true
|
||||
base:
|
||||
description: 'PR base branch; must be the branch checked out in the workspace'
|
||||
default: 'main'
|
||||
title:
|
||||
description: 'PR title'
|
||||
required: true
|
||||
commit-message:
|
||||
description: 'Commit message; defaults to the title'
|
||||
default: ''
|
||||
body:
|
||||
description: 'PR body'
|
||||
default: ''
|
||||
add-paths:
|
||||
description: 'Newline-separated pathspecs to commit; all changes when empty'
|
||||
default: ''
|
||||
labels:
|
||||
description: 'Newline-separated labels'
|
||||
default: ''
|
||||
outputs:
|
||||
number:
|
||||
description: 'PR number'
|
||||
value: ${{ steps.pr.outputs.pull-request-number }}
|
||||
url:
|
||||
description: 'PR URL'
|
||||
value: ${{ steps.pr.outputs.pull-request-url }}
|
||||
operation:
|
||||
description: 'created, updated, closed or none'
|
||||
value: ${{ steps.pr.outputs.pull-request-operation }}
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Open or update the PR
|
||||
id: pr
|
||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
||||
with:
|
||||
token: ${{ inputs.token }}
|
||||
branch: ${{ inputs.branch }}
|
||||
base: ${{ inputs.base }}
|
||||
title: ${{ inputs.title }}
|
||||
commit-message: ${{ inputs.commit-message || inputs.title }}
|
||||
body: ${{ inputs.body }}
|
||||
add-paths: ${{ inputs.add-paths }}
|
||||
labels: ${{ inputs.labels }}
|
||||
delete-branch: true
|
||||
|
||||
# No merge method is passed; the queue sets it. Re-requested on every update, since a
|
||||
# push can clear the request. GitHub refuses it on a PR that is already mergeable, so
|
||||
# that case merges directly.
|
||||
- name: Enable auto-merge
|
||||
if: ${{ steps.pr.outputs.pull-request-operation == 'created' || steps.pr.outputs.pull-request-operation == 'updated' }}
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ inputs.token }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
PR_NUMBER: ${{ steps.pr.outputs.pull-request-number }}
|
||||
run: |
|
||||
PR_ID=$(gh pr view "$PR_NUMBER" --json id --jq .id)
|
||||
QUERY=$(cat <<'GQL'
|
||||
mutation($id: ID!) {
|
||||
enablePullRequestAutoMerge(input: { pullRequestId: $id }) {
|
||||
pullRequest { number autoMergeRequest { enabledAt } }
|
||||
}
|
||||
}
|
||||
GQL
|
||||
)
|
||||
gh api graphql -f query="$QUERY" -F id="$PR_ID" \
|
||||
|| gh pr merge "$PR_NUMBER"
|
||||
@@ -28,8 +28,6 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
statuses: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
|
||||
@@ -41,13 +39,14 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
determine-tags:
|
||||
runs-on: ubuntu-26.04-arm
|
||||
runs-on: ubuntu-slim
|
||||
timeout-minutes: 10
|
||||
outputs:
|
||||
tag_to_process: ${{ steps.calculate_tags.outputs.tag_to_process }}
|
||||
release_name: ${{ steps.calculate_tags.outputs.release_name }}
|
||||
final_tag: ${{ steps.calculate_tags.outputs.final_tag }}
|
||||
from_channel: ${{ steps.calculate_tags.outputs.from_channel }}
|
||||
version_name: ${{ steps.version.outputs.version_name }}
|
||||
version_code: ${{ steps.version.outputs.version_code }}
|
||||
steps:
|
||||
# Internal releases are exempt: Play internal testing skips full review,
|
||||
# so only promotions (closed/open/production) can clobber an in-flight
|
||||
@@ -66,9 +65,14 @@ jobs:
|
||||
|
||||
- name: Calculate tags
|
||||
id: calculate_tags
|
||||
env:
|
||||
BASE_VERSION: ${{ inputs.base_version }}
|
||||
CHANNEL: ${{ inputs.channel }}
|
||||
run: |
|
||||
BASE_VERSION="${{ inputs.base_version }}"
|
||||
CHANNEL="${{ inputs.channel }}"
|
||||
if [[ ! "$BASE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "::error::base_version '$BASE_VERSION' is not X.Y.Z (e.g., 2.8.3)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$CHANNEL" == "internal" ]]; then
|
||||
# This is a new build, create a new internal tag
|
||||
@@ -84,7 +88,6 @@ jobs:
|
||||
echo "Calculated new tag: $NEW_TAG"
|
||||
{
|
||||
echo "tag_to_process=$NEW_TAG"
|
||||
echo "release_name=$NEW_TAG"
|
||||
echo "final_tag=$NEW_TAG"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
@@ -121,17 +124,49 @@ jobs:
|
||||
NEW_TAG="v${BASE_VERSION}"
|
||||
fi
|
||||
|
||||
echo "New release name will be: $NEW_TAG"
|
||||
echo "Final tag will be: $NEW_TAG"
|
||||
{
|
||||
echo "from_channel=${FROM_CHANNEL}"
|
||||
echo "tag_to_process=${LATEST_TAG_TO_PROMOTE}"
|
||||
echo "release_name=${NEW_TAG}"
|
||||
echo "final_tag=${NEW_TAG}"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
# Name from the tag, code from the commit count plus VERSION_CODE_OFFSET, both at the
|
||||
# commit being released: for a promotion the promoted tag's, which main has moved past.
|
||||
- name: Calculate version
|
||||
id: version
|
||||
env:
|
||||
BASE_VERSION: ${{ inputs.base_version }}
|
||||
CHANNEL: ${{ inputs.channel }}
|
||||
TAG: ${{ steps.calculate_tags.outputs.tag_to_process }}
|
||||
run: |
|
||||
if [[ "$CHANNEL" == "internal" ]]; then REF=HEAD; else REF="$TAG"; fi
|
||||
git show "${REF}:config.properties" > "$RUNNER_TEMP/config.properties"
|
||||
|
||||
# The release highlights and the Play what's-new are looked up by VERSION_NAME_BASE.
|
||||
CONFIG_BASE=$(sed -n 's/^VERSION_NAME_BASE=//p' "$RUNNER_TEMP/config.properties")
|
||||
if [[ "$CONFIG_BASE" != "$BASE_VERSION" ]]; then
|
||||
echo "::error::base_version is $BASE_VERSION but config.properties at $REF has VERSION_NAME_BASE=${CONFIG_BASE:-<unset>}."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION_NAME=$(echo "$TAG" | sed 's/-.*//' | sed 's/v//')
|
||||
VERSION_CODE_OFFSET=$(grep '^VERSION_CODE_OFFSET=' "$RUNNER_TEMP/config.properties" | cut -d'=' -f2 || true)
|
||||
if ! [[ "$VERSION_CODE_OFFSET" =~ ^[0-9]+$ ]]; then
|
||||
echo "::error::VERSION_CODE_OFFSET from config.properties is not numeric: '$VERSION_CODE_OFFSET'"
|
||||
exit 1
|
||||
fi
|
||||
VERSION_CODE=$(( $(git rev-list --count "$REF") + VERSION_CODE_OFFSET ))
|
||||
|
||||
echo "Version: $VERSION_NAME ($VERSION_CODE) from $REF"
|
||||
{
|
||||
echo "version_name=$VERSION_NAME"
|
||||
echo "version_code=$VERSION_CODE"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
shell: bash
|
||||
|
||||
- name: Create and Push Release Tag
|
||||
if: ${{ !inputs.dry_run && inputs.channel == 'internal' }}
|
||||
env:
|
||||
@@ -148,10 +183,8 @@ jobs:
|
||||
uses: ./.github/workflows/release.yml
|
||||
with:
|
||||
tag_name: ${{ needs.determine-tags.outputs.final_tag }}
|
||||
channel: ${{ inputs.channel }}
|
||||
base_version: ${{ inputs.base_version }}
|
||||
build_desktop: true
|
||||
build_flatpak_src: true
|
||||
version_name: ${{ needs.determine-tags.outputs.version_name }}
|
||||
version_code: ${{ needs.determine-tags.outputs.version_code }}
|
||||
secrets: inherit
|
||||
|
||||
call-promote-workflow:
|
||||
@@ -164,38 +197,65 @@ jobs:
|
||||
# so call-release-workflow doesn't carry it.
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
statuses: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
actions: write
|
||||
uses: ./.github/workflows/promote.yml
|
||||
with:
|
||||
tag_name: ${{ needs.determine-tags.outputs.tag_to_process }}
|
||||
release_name: ${{ needs.determine-tags.outputs.release_name }}
|
||||
final_tag: ${{ needs.determine-tags.outputs.final_tag }}
|
||||
channel: ${{ inputs.channel }}
|
||||
base_version: ${{ inputs.base_version }}
|
||||
from_channel: ${{ needs.determine-tags.outputs.from_channel }}
|
||||
version_code: ${{ needs.determine-tags.outputs.version_code }}
|
||||
secrets: inherit
|
||||
|
||||
# A production promotion stamps CHANGELOG.md itself; every other cut refreshes [Unreleased].
|
||||
update-changelog:
|
||||
needs: [call-release-workflow, call-promote-workflow]
|
||||
if: >-
|
||||
${{ !cancelled() && !inputs.dry_run && inputs.channel != 'production'
|
||||
&& (needs.call-release-workflow.result == 'success' || needs.call-promote-workflow.result == 'success') }}
|
||||
runs-on: ubuntu-slim
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
actions: write
|
||||
steps:
|
||||
- name: Dispatch Update Changelog
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
run: gh workflow run update-changelog.yml --ref main
|
||||
|
||||
cleanup-on-failure:
|
||||
needs: [determine-tags, call-release-workflow]
|
||||
if: ${{ (failure() || cancelled()) && !inputs.dry_run && inputs.channel == 'internal' }}
|
||||
runs-on: ubuntu-26.04-arm
|
||||
runs-on: ubuntu-slim
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: write
|
||||
actions: read
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Delete Failed or Cancelled Tag
|
||||
env:
|
||||
FINAL_TAG: ${{ needs.determine-tags.outputs.final_tag }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
RUN_ID: ${{ github.run_id }}
|
||||
run: |
|
||||
if [ -n "$FINAL_TAG" ]; then
|
||||
echo "Release workflow failed or was cancelled. Deleting tag $FINAL_TAG to allow a clean retry..."
|
||||
git push origin :refs/tags/"$FINAL_TAG" || echo "Tag was not pushed or already deleted."
|
||||
else
|
||||
if [ -z "$FINAL_TAG" ]; then
|
||||
echo "No tag was created to delete."
|
||||
exit 0
|
||||
fi
|
||||
# Play keeps an uploaded versionCode, so once publish-play succeeded the tag stays with it.
|
||||
PLAY=$(gh api "repos/$REPO/actions/runs/$RUN_ID/jobs?per_page=100" \
|
||||
--jq '[.jobs[] | select(.name | endswith("publish-play")) | .conclusion][0] // ""') || {
|
||||
echo "::warning::Could not read this run's jobs, so $FINAL_TAG stays. Delete it by hand if Play has no build from it."
|
||||
exit 0
|
||||
}
|
||||
if [ "$PLAY" = "success" ]; then
|
||||
echo "::warning::Play already has the build from $FINAL_TAG, so the tag stays. Re-run the failed jobs to finish the release."
|
||||
exit 0
|
||||
fi
|
||||
echo "Release workflow failed or was cancelled. Deleting tag $FINAL_TAG to allow a clean retry..."
|
||||
git push origin :refs/tags/"$FINAL_TAG" || echo "Tag was not pushed or already deleted."
|
||||
@@ -46,11 +46,12 @@ jobs:
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@14594264cd68ce8a2345dd349bc3d138a4ef85c8 # v1.327.0
|
||||
with:
|
||||
ruby-version: '4.0.7'
|
||||
bundler-cache: true
|
||||
|
||||
- name: Decode Play Store credentials
|
||||
run: echo '${{ secrets.GOOGLE_PLAY_JSON_KEY }}' > fastlane/play-store-credentials.json
|
||||
env:
|
||||
GOOGLE_PLAY_JSON_KEY: ${{ secrets.GOOGLE_PLAY_JSON_KEY }}
|
||||
run: printf '%s\n' "$GOOGLE_PLAY_JSON_KEY" > fastlane/play-store-credentials.json
|
||||
|
||||
- name: Upload listing
|
||||
env:
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
name: Play Rollout
|
||||
|
||||
# Moves the staged rollout already on a Play track: the one inProgress release a promotion
|
||||
# leaves there (production at 10%, beta at 50%). rollout widens it to `fraction`, complete
|
||||
# ships it to every user, halt stops it where it is. supply acts only on inProgress releases,
|
||||
# so a halted release is resumed or completed in the Play Console.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
track:
|
||||
description: 'Play track holding the staged release'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- production
|
||||
- beta
|
||||
action:
|
||||
description: 'rollout widens it to fraction, complete ships it to everyone, halt stops it'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- rollout
|
||||
- complete
|
||||
- halt
|
||||
fraction:
|
||||
description: 'rollout only: the new user fraction, above the current one and below 1 (e.g., 0.5)'
|
||||
required: false
|
||||
type: string
|
||||
no_review_in_flight:
|
||||
description: 'I checked Publishing overview > Submission activity and no submission is In review. A committed rollout change CANCELS and RESTARTS any review in flight.'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
# Not the promotion's group: a group keeps one pending run, so this would cancel a pending promotion.
|
||||
concurrency:
|
||||
group: play-rollout
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
rollout:
|
||||
if: github.repository == 'meshtastic/Meshtastic-Android'
|
||||
runs-on: ubuntu-26.04-arm
|
||||
timeout-minutes: 15
|
||||
env:
|
||||
TRACK: ${{ inputs.track }}
|
||||
ACTION: ${{ inputs.action }}
|
||||
steps:
|
||||
- name: Require review-in-flight confirmation
|
||||
if: ${{ !inputs.no_review_in_flight }}
|
||||
run: |
|
||||
echo "::error::Rollout change blocked: confirm no Play review is in flight. Check Play Console > Publishing overview > Submission activity; if a submission shows 'In review', wait. If clear, re-dispatch with 'no_review_in_flight' checked."
|
||||
exit 1
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@14594264cd68ce8a2345dd349bc3d138a4ef85c8 # v1.327.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
- name: Decode Play Store credentials
|
||||
env:
|
||||
GOOGLE_PLAY_JSON_KEY: ${{ secrets.GOOGLE_PLAY_JSON_KEY }}
|
||||
run: printf '%s\n' "$GOOGLE_PLAY_JSON_KEY" > fastlane/play-store-credentials.json
|
||||
|
||||
# Exactly one inProgress release, or nothing is changed: that release's version code
|
||||
# pins supply's selection, and its fraction is what a halt keeps.
|
||||
- name: Find the staged release
|
||||
id: staged
|
||||
env:
|
||||
FRACTION: ${{ inputs.fraction }}
|
||||
run: |
|
||||
bundle exec fastlane play_track_releases track:"$TRACK" out:"$RUNNER_TEMP/before.json"
|
||||
STAGED=$(jq -c '[.[] | select(.status == "inProgress")]' "$RUNNER_TEMP/before.json")
|
||||
if [[ $(jq length <<< "$STAGED") -ne 1 ]]; then
|
||||
echo "::error::Track '$TRACK' needs exactly one inProgress release; it holds: $(jq -c . "$RUNNER_TEMP/before.json")"
|
||||
exit 1
|
||||
fi
|
||||
VERSION_CODE=$(jq -r '.[0].version_codes | max' <<< "$STAGED")
|
||||
CURRENT=$(jq -r '.[0].user_fraction // empty' <<< "$STAGED")
|
||||
if [[ ! "$VERSION_CODE" =~ ^[0-9]+$ || ! "$CURRENT" =~ ^0?\.[0-9]+$ ]]; then
|
||||
echo "::error::The inProgress release on '$TRACK' has no usable version code or fraction: $STAGED"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
STATUS=""
|
||||
case "$ACTION" in
|
||||
rollout)
|
||||
if [[ ! "$FRACTION" =~ ^0?\.[0-9]+$ ]] || ! awk -v c="$CURRENT" -v n="$FRACTION" 'BEGIN { exit !(n > c && n < 1) }'; then
|
||||
echo "::error::fraction '$FRACTION' must be above the current $CURRENT and below 1; use complete to ship to everyone."
|
||||
exit 1
|
||||
fi
|
||||
ROLLOUT="$FRACTION"
|
||||
;;
|
||||
complete) ROLLOUT=1 ;;
|
||||
halt)
|
||||
ROLLOUT="$CURRENT"
|
||||
STATUS=halted
|
||||
;;
|
||||
esac
|
||||
echo "versionCode $VERSION_CODE on '$TRACK' at $CURRENT: $ACTION to $ROLLOUT${STATUS:+ ($STATUS)}"
|
||||
{
|
||||
echo "version_code=$VERSION_CODE"
|
||||
echo "current=$CURRENT"
|
||||
echo "rollout=$ROLLOUT"
|
||||
echo "status=$STATUS"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Every upload is skipped, so the edit changes only this release's status and fraction.
|
||||
- name: Change the rollout
|
||||
env:
|
||||
VERSION_CODE: ${{ steps.staged.outputs.version_code }}
|
||||
ROLLOUT: ${{ steps.staged.outputs.rollout }}
|
||||
STATUS: ${{ steps.staged.outputs.status }}
|
||||
run: |
|
||||
bundle exec fastlane supply \
|
||||
--track "$TRACK" \
|
||||
--version_code "$VERSION_CODE" \
|
||||
--rollout "$ROLLOUT" \
|
||||
${STATUS:+--release_status "$STATUS"} \
|
||||
--skip_upload_apk \
|
||||
--skip_upload_aab \
|
||||
--skip_upload_metadata \
|
||||
--skip_upload_changelogs \
|
||||
--skip_upload_images \
|
||||
--skip_upload_screenshots
|
||||
|
||||
# supply exits 0 even when its edit changed nothing. The script's verify counts only
|
||||
# completed and inProgress releases, so a halt is confirmed from a fresh read instead.
|
||||
- name: Verify the release on the track
|
||||
env:
|
||||
VERSION_CODE: ${{ steps.staged.outputs.version_code }}
|
||||
CURRENT: ${{ steps.staged.outputs.current }}
|
||||
ROLLOUT: ${{ steps.staged.outputs.rollout }}
|
||||
run: |
|
||||
PKG=$(grep '^APPLICATION_ID=' config.properties | cut -d'=' -f2)
|
||||
if [[ "$ACTION" != "halt" ]]; then
|
||||
bash scripts/play-track-preflight.sh \
|
||||
fastlane/play-store-credentials.json "$PKG" "$TRACK" "$VERSION_CODE" verify
|
||||
fi
|
||||
case "$ACTION" in
|
||||
rollout) WANT=inProgress ;;
|
||||
complete) WANT=completed ;;
|
||||
halt) WANT=halted ;;
|
||||
esac
|
||||
bundle exec fastlane play_track_releases track:"$TRACK" out:"$RUNNER_TEMP/after.json"
|
||||
AFTER=$(jq -c --argjson vc "$VERSION_CODE" '[.[] | select(.version_codes | index($vc))] | first // empty' "$RUNNER_TEMP/after.json")
|
||||
if [[ "$(jq -r '.status' <<< "${AFTER:-null}")" != "$WANT" ]]; then
|
||||
echo "::error::versionCode $VERSION_CODE on '$TRACK' is not $WANT after the change: ${AFTER:-absent}"
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$ACTION" == "rollout" ]] && ! awk -v a="$(jq -r '.user_fraction' <<< "$AFTER")" -v w="$ROLLOUT" 'BEGIN { exit !(a == w) }'; then
|
||||
echo "::error::versionCode $VERSION_CODE on '$TRACK' is not at $ROLLOUT after the change: $AFTER"
|
||||
exit 1
|
||||
fi
|
||||
{
|
||||
echo "## Play rollout: ${TRACK}"
|
||||
echo
|
||||
echo "versionCode ${VERSION_CODE}: ${ACTION}, from ${CURRENT} to $(jq -r '.user_fraction // "all users"' <<< "$AFTER") (${WANT})."
|
||||
echo
|
||||
echo "A change Play could not send for review on its own waits under Publishing overview in the Play Console."
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Clean up credentials
|
||||
if: always()
|
||||
run: rm -f fastlane/play-store-credentials.json
|
||||
+225
-183
@@ -11,18 +11,10 @@ on:
|
||||
description: 'The tag that triggered the release'
|
||||
required: true
|
||||
type: string
|
||||
release_name:
|
||||
description: 'The desired name for the GitHub release'
|
||||
required: true
|
||||
type: string
|
||||
final_tag:
|
||||
description: 'The final tag for the release'
|
||||
required: true
|
||||
type: string
|
||||
commit_sha:
|
||||
description: 'The commit SHA to tag'
|
||||
required: false
|
||||
type: string
|
||||
channel:
|
||||
description: 'The channel to promote to'
|
||||
required: true
|
||||
@@ -31,29 +23,26 @@ on:
|
||||
description: 'The channel to promote from'
|
||||
required: true
|
||||
type: string
|
||||
version_code:
|
||||
description: 'The version code of the build being promoted'
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
GSERVICES:
|
||||
required: true
|
||||
KEYSTORE:
|
||||
required: true
|
||||
KEYSTORE_FILENAME:
|
||||
required: true
|
||||
KEYSTORE_PROPERTIES:
|
||||
required: true
|
||||
DATADOG_APPLICATION_ID:
|
||||
required: true
|
||||
DATADOG_CLIENT_TOKEN:
|
||||
required: true
|
||||
GOOGLE_MAPS_API_KEY:
|
||||
required: true
|
||||
GOOGLE_PLAY_JSON_KEY:
|
||||
required: true
|
||||
GRADLE_ENCRYPTION_KEY:
|
||||
required: true
|
||||
DISCORD_WEBHOOK_ANDROID:
|
||||
required: false
|
||||
HOMEBREW_TAP_TOKEN:
|
||||
required: false
|
||||
CROWDIN_GITHUB_TOKEN:
|
||||
required: false
|
||||
FLATHUB_TOKEN:
|
||||
required: false
|
||||
# Read only for presence, so the checklist says whether each store workflow will skip.
|
||||
WINGET_TOKEN:
|
||||
required: false
|
||||
MSSTORE_PRODUCT_ID:
|
||||
required: false
|
||||
|
||||
# Never cancel a promotion mid-flight: being killed between the Play edit
|
||||
# commit and the GitHub release/tag update leaves the two disagreeing. The
|
||||
@@ -65,59 +54,11 @@ concurrency:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
statuses: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
|
||||
jobs:
|
||||
prepare-build-info:
|
||||
runs-on: ubuntu-26.04-arm
|
||||
timeout-minutes: 10
|
||||
outputs:
|
||||
APP_VERSION_NAME: ${{ steps.prep_version.outputs.APP_VERSION_NAME }}
|
||||
APP_VERSION_CODE: ${{ steps.calculate_version_code.outputs.versionCode }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ inputs.commit_sha || inputs.tag_name }}
|
||||
fetch-depth: 0
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Prep APP_VERSION_NAME
|
||||
id: prep_version
|
||||
env:
|
||||
INPUT_TAG_NAME: ${{ inputs.tag_name }}
|
||||
run: |
|
||||
VERSION_NAME=$(echo "$INPUT_TAG_NAME" | sed 's/-.*//' | sed 's/v//')
|
||||
echo "APP_VERSION_NAME=$VERSION_NAME" >> "$GITHUB_OUTPUT"
|
||||
echo "Parsed Version: $VERSION_NAME"
|
||||
|
||||
- name: Extract VERSION_CODE_OFFSET from config.properties
|
||||
id: get_version_code_offset
|
||||
run: |
|
||||
OFFSET=$(grep '^VERSION_CODE_OFFSET=' config.properties | cut -d'=' -f2)
|
||||
echo "VERSION_CODE_OFFSET=$OFFSET" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Calculate Version Code from Git Commit Count
|
||||
id: calculate_version_code
|
||||
env:
|
||||
VERSION_CODE_OFFSET: ${{ steps.get_version_code_offset.outputs.VERSION_CODE_OFFSET }}
|
||||
run: |
|
||||
COMMIT_COUNT=$(git rev-list --count HEAD)
|
||||
if ! [[ "$VERSION_CODE_OFFSET" =~ ^[0-9]+$ ]]; then
|
||||
echo "::error::VERSION_CODE_OFFSET from config.properties is not numeric: '$VERSION_CODE_OFFSET'"
|
||||
exit 1
|
||||
fi
|
||||
VERSION_CODE=$((COMMIT_COUNT + VERSION_CODE_OFFSET))
|
||||
echo "versionCode=$VERSION_CODE" >> "$GITHUB_OUTPUT"
|
||||
shell: bash
|
||||
|
||||
promote-release:
|
||||
runs-on: ubuntu-26.04-arm
|
||||
timeout-minutes: 30
|
||||
needs: [ prepare-build-info ]
|
||||
outputs:
|
||||
already_on_track: ${{ steps.preflight.outputs.already_on_track }}
|
||||
screenshots: ${{ steps.screenshots.outputs.found }}
|
||||
@@ -130,16 +71,17 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ inputs.commit_sha || inputs.tag_name }}
|
||||
ref: ${{ inputs.tag_name }}
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@14594264cd68ce8a2345dd349bc3d138a4ef85c8 # v1.327.0
|
||||
with:
|
||||
ruby-version: '4.0.7'
|
||||
bundler-cache: true
|
||||
|
||||
- name: Decode Play Store credentials
|
||||
run: echo '${{ secrets.GOOGLE_PLAY_JSON_KEY }}' > fastlane/play-store-credentials.json
|
||||
env:
|
||||
GOOGLE_PLAY_JSON_KEY: ${{ secrets.GOOGLE_PLAY_JSON_KEY }}
|
||||
run: printf '%s\n' "$GOOGLE_PLAY_JSON_KEY" > fastlane/play-store-credentials.json
|
||||
|
||||
# A re-dispatched promotion whose versionCode is already live on the
|
||||
# target track must no-op: every redundant `supply` commit creates a new
|
||||
@@ -169,7 +111,7 @@ jobs:
|
||||
- name: Preflight — is this versionCode already on the target track?
|
||||
id: preflight
|
||||
env:
|
||||
VERSION_CODE: ${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }}
|
||||
VERSION_CODE: ${{ inputs.version_code }}
|
||||
run: |
|
||||
PKG=$(grep '^APPLICATION_ID=' config.properties | cut -d'=' -f2)
|
||||
bash .workflow-ref/scripts/play-track-preflight.sh \
|
||||
@@ -185,7 +127,7 @@ jobs:
|
||||
- name: Promote to next channel
|
||||
if: ${{ steps.preflight.outputs.already_on_track != 'true' }}
|
||||
env:
|
||||
VERSION_CODE: ${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }}
|
||||
VERSION_CODE: ${{ inputs.version_code }}
|
||||
run: |
|
||||
bundle exec fastlane supply \
|
||||
--track "$FROM_TRACK" \
|
||||
@@ -202,7 +144,7 @@ jobs:
|
||||
- name: Verify versionCode landed on the target track
|
||||
if: ${{ steps.preflight.outputs.already_on_track != 'true' }}
|
||||
env:
|
||||
VERSION_CODE: ${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }}
|
||||
VERSION_CODE: ${{ inputs.version_code }}
|
||||
run: |
|
||||
PKG=$(grep '^APPLICATION_ID=' config.properties | cut -d'=' -f2)
|
||||
bash .workflow-ref/scripts/play-track-preflight.sh \
|
||||
@@ -250,51 +192,50 @@ jobs:
|
||||
run: rm -f fastlane/play-store-credentials.json
|
||||
|
||||
update-github-release:
|
||||
runs-on: ubuntu-26.04-arm
|
||||
runs-on: ubuntu-slim
|
||||
timeout-minutes: 10
|
||||
needs: [ prepare-build-info, promote-release ]
|
||||
needs: [ promote-release ]
|
||||
# actions: write is scoped here — only this job's publish-workflow
|
||||
# dispatch needs it, and the other jobs must not get it. Job-level
|
||||
# permissions replace the workflow-level block, so the full set this
|
||||
# job uses is listed.
|
||||
# job uses is listed. Its PRs are opened with CROWDIN_GITHUB_TOKEN.
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
statuses: write
|
||||
actions: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ inputs.commit_sha || inputs.tag_name }}
|
||||
ref: ${{ inputs.tag_name }}
|
||||
fetch-depth: 0
|
||||
submodules: 'recursive'
|
||||
|
||||
# Same reasoning as promote-release: scripts come from the caller's commit,
|
||||
# not the tag, so a tag cut before a script landed still gets it.
|
||||
# Same reasoning as promote-release: scripts and the bot-pr action come from the
|
||||
# caller's commit, not the tag, so a tag cut before either landed still gets it.
|
||||
- name: Checkout release scripts from caller commit
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
path: .workflow-ref
|
||||
sparse-checkout: scripts
|
||||
|
||||
- name: Push Git Tag on Success
|
||||
if: ${{ inputs.commit_sha != '' }}
|
||||
run: |
|
||||
git tag ${{ inputs.final_tag }} ${{ inputs.commit_sha }}
|
||||
git push origin ${{ inputs.final_tag }}
|
||||
sparse-checkout: |
|
||||
scripts
|
||||
.github/actions/bot-pr
|
||||
|
||||
# A rerun finds the release already moved to the final tag, so that is looked up first.
|
||||
- name: Update GitHub Release with gh CLI
|
||||
id: release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TAG: ${{ inputs.tag_name }}
|
||||
FINAL_TAG: ${{ inputs.final_tag }}
|
||||
VERSION_CODE: ${{ inputs.version_code }}
|
||||
PRERELEASE: ${{ inputs.channel != 'production' }}
|
||||
run: |
|
||||
gh release edit ${{ inputs.tag_name }} \
|
||||
--tag ${{ inputs.final_tag }} \
|
||||
--title "${{ inputs.release_name }} (${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }})" \
|
||||
CURRENT=$(gh release view "$FINAL_TAG" --json tagName --jq .tagName 2>/dev/null || echo "$TAG")
|
||||
gh release edit "$CURRENT" \
|
||||
--tag "$FINAL_TAG" \
|
||||
--title "$FINAL_TAG ($VERSION_CODE)" \
|
||||
--draft=false \
|
||||
--prerelease=${{ inputs.channel != 'production' }}
|
||||
--prerelease="$PRERELEASE"
|
||||
|
||||
# The draft's notes were generated at the internal cut and cover only the
|
||||
# PRs since the previous published pre-release. A production release is
|
||||
@@ -386,18 +327,28 @@ jobs:
|
||||
done
|
||||
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.
|
||||
- name: Dispatch scheduled updates
|
||||
id: sched
|
||||
if: ${{ inputs.channel != 'internal' }}
|
||||
continue-on-error: true
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: gh workflow run scheduled-updates.yml --ref main
|
||||
|
||||
# bot-pr resets the checked-out branch to its origin copy before branching the PR,
|
||||
# so the edits below are made on a local main, never on a branch origin lacks.
|
||||
- name: Stamp CHANGELOG.md for release
|
||||
id: stamp
|
||||
if: ${{ inputs.channel == 'production' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
VERSION: ${{ inputs.base_version }}
|
||||
run: |
|
||||
DATE=$(date -u +%Y-%m-%d)
|
||||
|
||||
# Checkout CHANGELOG.md from main (we're on a tag checkout)
|
||||
git fetch origin main
|
||||
git checkout -b "changelog/v${VERSION}" origin/main
|
||||
git checkout -B main origin/main
|
||||
|
||||
# The full-range notes from the step above, minus the boilerplate.
|
||||
# The file is the record whether or not the release edit went
|
||||
@@ -444,98 +395,66 @@ jobs:
|
||||
f.write(new_content)
|
||||
" "$VERSION" "$DATE" "$FLAT_NOTES"
|
||||
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add CHANGELOG.md
|
||||
git diff --cached --quiet || {
|
||||
BRANCH="automation/changelog-v${VERSION}"
|
||||
git checkout -B "$BRANCH"
|
||||
git commit -m "docs: release CHANGELOG.md for v${VERSION}"
|
||||
git push origin "$BRANCH" --force
|
||||
|
||||
PR_URL=$(gh pr create \
|
||||
--title "docs: release CHANGELOG.md for v${VERSION}" \
|
||||
--body "Automated changelog stamp for production release v${VERSION}." \
|
||||
--head "$BRANCH" \
|
||||
--base main \
|
||||
--label "automation" \
|
||||
--label "skip-changelog")
|
||||
echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Post required commit status so the PR isn't blocked
|
||||
COMMIT_SHA=$(git rev-parse HEAD)
|
||||
gh api "repos/${{ github.repository }}/statuses/${COMMIT_SHA}" \
|
||||
-f state="success" \
|
||||
-f context="Check Workflow Status" \
|
||||
-f description="Skipped — changelog-only PR"
|
||||
}
|
||||
- name: Open the CHANGELOG.md PR
|
||||
id: stamp_pr
|
||||
if: ${{ inputs.channel == 'production' }}
|
||||
uses: ./.workflow-ref/.github/actions/bot-pr
|
||||
with:
|
||||
token: ${{ secrets.CROWDIN_GITHUB_TOKEN }}
|
||||
branch: automation/changelog-v${{ inputs.base_version }}
|
||||
title: 'docs: release CHANGELOG.md for v${{ inputs.base_version }}'
|
||||
body: 'Automated changelog stamp for production release v${{ inputs.base_version }}.'
|
||||
add-paths: CHANGELOG.md
|
||||
labels: |
|
||||
automation
|
||||
skip-changelog
|
||||
|
||||
# F-Droid and IzzyOnDroid read fastlane/ straight from git, so the committed set
|
||||
# follows what production shipped: the fdroid-flavor captures come back from the
|
||||
# release into a bot PR that merges itself, with the desktop set beside them. The
|
||||
# Flathub metainfo reads the release assets directly and needs nothing here.
|
||||
# Nothing to open when the files already match.
|
||||
- name: Refresh the committed store screenshots
|
||||
id: shots_pr
|
||||
# Nothing is opened when the files already match.
|
||||
- name: Stage the store screenshots from the release
|
||||
id: shots
|
||||
if: ${{ inputs.channel == 'production' }}
|
||||
continue-on-error: true
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TAG: ${{ inputs.final_tag }}
|
||||
VERSION: ${{ inputs.base_version }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
ASSET=$(gh release view "$TAG" --json assets --jq '.assets[].name | select(startswith("store-listing-screenshots-fdroid-"))' | head -1)
|
||||
if [[ -z "$ASSET" ]]; then
|
||||
echo "::warning::No rendered screenshots attached to $TAG; the committed set stays."
|
||||
echo "refreshed=none" >> "$GITHUB_OUTPUT"
|
||||
echo "found=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
gh release download "$TAG" --pattern "$ASSET" --pattern 'meshtastic-desktop-*.png' --dir "$RUNNER_TEMP/shots"
|
||||
git fetch origin main
|
||||
BRANCH="automation/store-screenshots-v${VERSION}"
|
||||
git checkout -B "$BRANCH" origin/main
|
||||
git checkout -B main origin/main
|
||||
unzip -qo "$RUNNER_TEMP/shots/$ASSET" -d fastlane/metadata/android/en-US/images
|
||||
cp "$RUNNER_TEMP"/shots/meshtastic-desktop-*.png desktopApp/packaging/linux/screenshots/
|
||||
git add fastlane/metadata/android/en-US/images desktopApp/packaging/linux/screenshots
|
||||
if git diff --cached --quiet; then
|
||||
echo "Committed screenshots already match v${VERSION}."
|
||||
echo "refreshed=match" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git commit -m "chore(store): refresh the committed screenshots from v${VERSION}"
|
||||
git push origin "$BRANCH" --force
|
||||
PR_URL=$(gh pr create \
|
||||
--title "chore(store): refresh the committed screenshots from v${VERSION}" \
|
||||
--body "The screenshots rendered for the v${VERSION} release, so the fastlane tree F-Droid reads matches what shipped." \
|
||||
--head "$BRANCH" \
|
||||
--base main \
|
||||
--label "automation" \
|
||||
--label "skip-changelog")
|
||||
echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT"
|
||||
# GITHUB_TOKEN pushes start no workflows: post the status the queue
|
||||
# requires, as the changelog stamp does, then let the queue take it.
|
||||
gh api "repos/${REPO}/statuses/$(git rev-parse HEAD)" \
|
||||
-f state="success" \
|
||||
-f context="Check Workflow Status" \
|
||||
-f description="Skipped — screenshot-only PR"
|
||||
PR_ID=$(gh pr view "$PR_URL" --json id --jq .id)
|
||||
QUERY=$(cat <<'GQL'
|
||||
mutation($id: ID!) {
|
||||
enablePullRequestAutoMerge(input: { pullRequestId: $id }) {
|
||||
pullRequest { number autoMergeRequest { enabledAt } }
|
||||
}
|
||||
}
|
||||
GQL
|
||||
)
|
||||
gh api graphql -f query="$QUERY" -F id="$PR_ID" || gh pr merge "$PR_URL"
|
||||
echo "found=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Refresh the committed store screenshots
|
||||
id: shots_pr
|
||||
if: ${{ steps.shots.outputs.found == 'true' }}
|
||||
continue-on-error: true
|
||||
uses: ./.workflow-ref/.github/actions/bot-pr
|
||||
with:
|
||||
token: ${{ secrets.CROWDIN_GITHUB_TOKEN }}
|
||||
branch: automation/store-screenshots-v${{ inputs.base_version }}
|
||||
title: 'chore(store): refresh the committed screenshots from v${{ inputs.base_version }}'
|
||||
body: 'The screenshots rendered for the v${{ inputs.base_version }} release, so the fastlane tree F-Droid reads matches what shipped.'
|
||||
add-paths: |
|
||||
fastlane/metadata/android/en-US/images
|
||||
desktopApp/packaging/linux/screenshots
|
||||
labels: |
|
||||
automation
|
||||
skip-changelog
|
||||
|
||||
# One place to read what this promotion did, what it dispatched, and what
|
||||
# is still done by hand. The store dispatches report the dispatch only;
|
||||
# each store workflow skips inside itself until its secrets exist. The
|
||||
# hand-done list stays out of the Discord post, which is an announcement.
|
||||
# is still done by hand. The hand-done list stays out of the Discord post,
|
||||
# which is an announcement.
|
||||
- name: Release checklist
|
||||
if: ${{ always() }}
|
||||
env:
|
||||
@@ -545,19 +464,37 @@ jobs:
|
||||
ALREADY_ON_TRACK: ${{ needs.promote-release.outputs.already_on_track }}
|
||||
SCREENSHOTS: ${{ needs.promote-release.outputs.screenshots }}
|
||||
LISTING: ${{ needs.promote-release.outputs.listing }}
|
||||
SHOTS_PR: ${{ steps.shots_pr.outputs.pr_url }}
|
||||
SHOTS: ${{ steps.shots.outcome }}
|
||||
SHOTS_FOUND: ${{ steps.shots.outputs.found }}
|
||||
SHOTS_PR: ${{ steps.shots_pr.outputs.url }}
|
||||
SHOTS_PR_OUTCOME: ${{ steps.shots_pr.outcome }}
|
||||
SHOTS_REFRESHED: ${{ steps.shots_pr.outputs.refreshed }}
|
||||
RELEASE: ${{ steps.release.outcome }}
|
||||
NOTES: ${{ steps.notes.outcome }}
|
||||
DOCS: ${{ steps.docs.outcome }}
|
||||
SCHED: ${{ steps.sched.outcome }}
|
||||
WINGET: ${{ steps.winget.outcome }}
|
||||
MSSTORE: ${{ steps.msstore.outcome }}
|
||||
BUMP: ${{ steps.bump.outcome }}
|
||||
STAMP: ${{ steps.stamp.outcome }}
|
||||
STAMP_PR: ${{ steps.stamp.outputs.pr_url }}
|
||||
STAMP_PR: ${{ steps.stamp_pr.outputs.url }}
|
||||
STAMP_PR_OUTCOME: ${{ steps.stamp_pr.outcome }}
|
||||
HAS_FLATHUB_TOKEN: ${{ secrets.FLATHUB_TOKEN != '' }}
|
||||
HAS_WINGET_TOKEN: ${{ secrets.WINGET_TOKEN != '' }}
|
||||
HAS_MSSTORE_PRODUCT_ID: ${{ secrets.MSSTORE_PRODUCT_ID != '' }}
|
||||
HAS_HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN != '' }}
|
||||
run: |
|
||||
row() { printf '| %s | %s |\n' "$1" "$2"; }
|
||||
# A store workflow skips inside itself when its secret is unset, so a successful
|
||||
# dispatch alone does not mean anything was submitted.
|
||||
store_row() {
|
||||
if [[ "$2" != "success" ]]; then
|
||||
row "$1" "$2"
|
||||
elif [[ "$3" != "true" ]]; then
|
||||
row "$1" "will skip: $4 unset"
|
||||
else
|
||||
row "$1" "dispatched"
|
||||
fi
|
||||
}
|
||||
{
|
||||
echo "## Release checklist: ${TAG} (${CHANNEL})"
|
||||
echo
|
||||
@@ -580,30 +517,56 @@ jobs:
|
||||
row "Play listing validated (dry run)" "$LISTING"
|
||||
fi
|
||||
row "Docs Release dispatched" "$DOCS"
|
||||
row "Scheduled Updates dispatched (Obtainium table)" "$SCHED"
|
||||
if [[ "$CHANNEL" == "production" ]]; then
|
||||
if [[ -n "$SHOTS_PR" ]]; then
|
||||
row "Committed screenshots refresh PR" "$SHOTS_PR"
|
||||
elif [[ "$SHOTS" != "success" ]]; then
|
||||
row "Committed screenshots refresh PR" "$SHOTS"
|
||||
elif [[ "$SHOTS_FOUND" != "true" ]]; then
|
||||
row "Committed screenshots refresh PR" "none needed (no rendered set attached)"
|
||||
elif [[ "$SHOTS_PR_OUTCOME" == "success" ]]; then
|
||||
row "Committed screenshots refresh PR" "none needed (${SHOTS_REFRESHED:-no change})"
|
||||
row "Committed screenshots refresh PR" "none needed (committed set matches)"
|
||||
else
|
||||
row "Committed screenshots refresh PR" "$SHOTS_PR_OUTCOME"
|
||||
fi
|
||||
row "Release notes rewritten (full range)" "$NOTES"
|
||||
row "CHANGELOG.md stamp" "$STAMP ${STAMP_PR:+- $STAMP_PR}"
|
||||
row "winget dispatched" "$WINGET"
|
||||
row "Microsoft Store dispatched" "$MSSTORE"
|
||||
if [[ -n "$STAMP_PR" ]]; then
|
||||
row "CHANGELOG.md stamp PR" "$STAMP_PR"
|
||||
elif [[ "$STAMP" == "success" && "$STAMP_PR_OUTCOME" == "success" ]]; then
|
||||
row "CHANGELOG.md stamp PR" "none needed (already stamped)"
|
||||
else
|
||||
row "CHANGELOG.md stamp PR" "stamp $STAMP, PR ${STAMP_PR_OUTCOME:-skipped}"
|
||||
fi
|
||||
store_row "winget" "$WINGET" "$HAS_WINGET_TOKEN" WINGET_TOKEN
|
||||
store_row "Microsoft Store" "$MSSTORE" "$HAS_MSSTORE_PRODUCT_ID" MSSTORE_PRODUCT_ID
|
||||
row "Version bump dispatched" "$BUMP"
|
||||
row "Post-Release Cleanup" "dispatched by Docs Release once /${TAG}/ is published"
|
||||
if [[ "$HAS_HOMEBREW_TAP_TOKEN" == "true" ]]; then
|
||||
row "Homebrew cask PR" "opened by the update-homebrew-cask job after this one"
|
||||
else
|
||||
row "Homebrew cask PR" "will skip: HOMEBREW_TAP_TOKEN unset"
|
||||
fi
|
||||
if [[ "$HAS_FLATHUB_TOKEN" == "true" ]]; then
|
||||
row "Flathub update PR" "opened by the update-flathub job after this one"
|
||||
else
|
||||
row "Flathub update PR" "will skip: FLATHUB_TOKEN unset"
|
||||
fi
|
||||
echo
|
||||
echo "Still by hand:"
|
||||
echo "- Play Console: the production rollout is staged; complete it there."
|
||||
echo "- Flathub: bump flathub/org.meshtastic.MeshtasticDesktop (tag, commit, gradle zip and sha256, flatpak-sources.json)."
|
||||
echo "- Play: the production rollout is staged; widen, complete or halt it with the Play Rollout workflow."
|
||||
if [[ "$HAS_FLATHUB_TOKEN" == "true" ]]; then
|
||||
echo "- Flathub: merge the update-flathub PR once its test build passes."
|
||||
else
|
||||
echo "- Flathub: bump flathub/org.meshtastic.MeshtasticDesktop (tag, commit, gradle zip and sha256, flatpak-sources.json)."
|
||||
fi
|
||||
echo "- Release notes: replace the placeholder <description> the version bump PR wrote for the next line."
|
||||
fi
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
# Announces once the release is published, whatever happened to the steps after it.
|
||||
- name: Notify Discord
|
||||
if: ${{ inputs.channel != 'internal' }}
|
||||
if: ${{ always() && steps.release.outcome == 'success' && inputs.channel != 'internal' }}
|
||||
env:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_ANDROID }}
|
||||
VERSION: ${{ inputs.final_tag }}
|
||||
@@ -654,14 +617,14 @@ jobs:
|
||||
# promoted to homebrew/cask, replace the tap PR with `brew bump-cask-pr`.
|
||||
update-homebrew-cask:
|
||||
if: ${{ inputs.channel == 'production' }}
|
||||
runs-on: ubuntu-26.04-arm
|
||||
runs-on: ubuntu-slim
|
||||
timeout-minutes: 15
|
||||
needs: [ update-github-release ]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ inputs.commit_sha || inputs.tag_name }}
|
||||
ref: ${{ inputs.tag_name }}
|
||||
|
||||
- name: Render cask and open PR against meshtastic/homebrew-tap
|
||||
env:
|
||||
@@ -713,3 +676,82 @@ jobs:
|
||||
--body "Automated cask bump for the ${TAG} production release of [Meshtastic-Android](https://github.com/${{ github.repository }}/releases/tag/${TAG})." \
|
||||
|| echo "PR already exists for $BRANCH; branch updated.")
|
||||
echo "- Homebrew cask: $PR_URL" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
# The same for flathub/org.meshtastic.MeshtasticDesktop: the tag and its commit, the Gradle
|
||||
# zip that tag's wrapper pins, and the release's flatpak-sources.json. The JBR, runtime and
|
||||
# patches stay as they are; Flathub's test build on the PR checks them against the tag.
|
||||
update-flathub:
|
||||
if: ${{ inputs.channel == 'production' }}
|
||||
runs-on: ubuntu-slim
|
||||
timeout-minutes: 15
|
||||
needs: [ update-github-release ]
|
||||
steps:
|
||||
# From the caller's commit, like the scripts in the jobs above.
|
||||
- name: Checkout the manifest bump script from caller commit
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
sparse-checkout: scripts/verify-flatpak
|
||||
|
||||
- name: Update the manifest and open PR against flathub/org.meshtastic.MeshtasticDesktop
|
||||
env:
|
||||
FLATHUB_TOKEN: ${{ secrets.FLATHUB_TOKEN }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TAG: ${{ inputs.final_tag }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
if [[ -z "$FLATHUB_TOKEN" ]]; then
|
||||
echo "::notice::FLATHUB_TOKEN not set; skipping the Flathub update."
|
||||
echo "- Flathub: skipped, FLATHUB_TOKEN not set" >> "$GITHUB_STEP_SUMMARY"
|
||||
exit 0
|
||||
fi
|
||||
VERSION=${TAG#v}
|
||||
|
||||
# The tag appears a moment after the undraft.
|
||||
TYPE="" COMMIT=""
|
||||
for _ in 1 2 3 4 5 6; do
|
||||
if OBJ=$(gh api "repos/${REPO}/git/ref/tags/${TAG}" --jq '.object.type + " " + .object.sha' 2>/dev/null); then
|
||||
read -r TYPE COMMIT <<< "$OBJ"
|
||||
break
|
||||
fi
|
||||
sleep 10
|
||||
done
|
||||
if [[ -z "$COMMIT" ]]; then
|
||||
echo "::error::Tag $TAG not found."
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$TYPE" == "tag" ]]; then
|
||||
COMMIT=$(gh api "repos/${REPO}/git/tags/${COMMIT}" --jq .object.sha)
|
||||
fi
|
||||
|
||||
gh api "repos/${REPO}/contents/gradle/wrapper/gradle-wrapper.properties?ref=${COMMIT}" \
|
||||
-H 'Accept: application/vnd.github.raw' > "$RUNNER_TEMP/gradle-wrapper.properties"
|
||||
gh release download "$TAG" --repo "$REPO" --pattern flatpak-sources.json --dir "$RUNNER_TEMP"
|
||||
jq empty "$RUNNER_TEMP/flatpak-sources.json"
|
||||
|
||||
FLATHUB="$RUNNER_TEMP/flathub"
|
||||
git clone "https://x-access-token:${FLATHUB_TOKEN}@github.com/flathub/org.meshtastic.MeshtasticDesktop.git" "$FLATHUB"
|
||||
BASE=$(git -C "$FLATHUB" rev-parse --abbrev-ref HEAD)
|
||||
BRANCH="update-${VERSION}"
|
||||
git -C "$FLATHUB" checkout -b "$BRANCH"
|
||||
python3 scripts/verify-flatpak/bump-flathub-manifest.py \
|
||||
"$FLATHUB/org.meshtastic.MeshtasticDesktop.yaml" "$TAG" "$COMMIT" "$RUNNER_TEMP/gradle-wrapper.properties"
|
||||
cp "$RUNNER_TEMP/flatpak-sources.json" "$FLATHUB/flatpak-sources.json"
|
||||
|
||||
cd "$FLATHUB"
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add org.meshtastic.MeshtasticDesktop.yaml flatpak-sources.json
|
||||
if git diff --cached --quiet; then
|
||||
echo "Flathub manifest already at ${TAG}."
|
||||
echo "- Flathub: already at ${TAG}" >> "$GITHUB_STEP_SUMMARY"
|
||||
exit 0
|
||||
fi
|
||||
git commit -m "Update to ${TAG}"
|
||||
git push -fu origin "$BRANCH"
|
||||
PR_URL=$(GH_TOKEN="$FLATHUB_TOKEN" gh pr create --repo flathub/org.meshtastic.MeshtasticDesktop \
|
||||
--head "$BRANCH" --base "$BASE" \
|
||||
--title "Update to ${TAG}" \
|
||||
--body "$(printf 'Update Meshtastic Desktop to version %s\nhttps://github.com/%s/releases/tag/%s\n' "$TAG" "$REPO" "$TAG")" \
|
||||
|| echo "PR already exists for $BRANCH; branch updated.")
|
||||
echo "- Flathub: $PR_URL" >> "$GITHUB_STEP_SUMMARY"
|
||||
+125
-175
@@ -3,32 +3,18 @@ name: Make Release
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
base_version:
|
||||
description: 'The base version for the release (e.g., 2.3.0)'
|
||||
required: true
|
||||
type: string
|
||||
tag_name:
|
||||
description: 'The tag that triggered the release'
|
||||
required: true
|
||||
type: string
|
||||
commit_sha:
|
||||
description: 'The commit SHA to build and tag'
|
||||
required: false
|
||||
type: string
|
||||
channel:
|
||||
description: 'The channel to create a release for or promote to'
|
||||
version_name:
|
||||
description: 'The version name the build carries (e.g., 2.3.0)'
|
||||
required: true
|
||||
type: string
|
||||
version_code:
|
||||
description: 'The version code the build carries'
|
||||
required: true
|
||||
type: string
|
||||
build_desktop:
|
||||
description: 'Whether to build the desktop distribution'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
build_flatpak_src:
|
||||
description: 'Whether to build the Flatpak sources'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
secrets:
|
||||
GSERVICES:
|
||||
required: true
|
||||
@@ -79,64 +65,19 @@ concurrency:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: read
|
||||
id-token: write
|
||||
attestations: write
|
||||
|
||||
jobs:
|
||||
prepare-build-info:
|
||||
runs-on: ubuntu-26.04-arm
|
||||
timeout-minutes: 10
|
||||
outputs:
|
||||
APP_VERSION_NAME: ${{ steps.prep_version.outputs.APP_VERSION_NAME }}
|
||||
APP_VERSION_CODE: ${{ steps.calculate_version_code.outputs.versionCode }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ inputs.tag_name }}
|
||||
fetch-depth: 0
|
||||
submodules: 'recursive'
|
||||
- name: Prep APP_VERSION_NAME
|
||||
id: prep_version
|
||||
env:
|
||||
INPUT_TAG_NAME: ${{ inputs.tag_name }}
|
||||
run: |
|
||||
VERSION_NAME=$(echo "$INPUT_TAG_NAME" | sed 's/-.*//' | sed 's/v//')
|
||||
echo "APP_VERSION_NAME=$VERSION_NAME" >> "$GITHUB_OUTPUT"
|
||||
echo "Parsed Version: $VERSION_NAME"
|
||||
|
||||
- name: Extract VERSION_CODE_OFFSET from config.properties
|
||||
id: get_version_code_offset
|
||||
run: |
|
||||
OFFSET=$(grep '^VERSION_CODE_OFFSET=' config.properties | cut -d'=' -f2)
|
||||
echo "VERSION_CODE_OFFSET=$OFFSET" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Calculate Version Code from Git Commit Count
|
||||
id: calculate_version_code
|
||||
env:
|
||||
VERSION_CODE_OFFSET: ${{ steps.get_version_code_offset.outputs.VERSION_CODE_OFFSET }}
|
||||
run: |
|
||||
COMMIT_COUNT=$(git rev-list --count HEAD)
|
||||
if ! [[ "$VERSION_CODE_OFFSET" =~ ^[0-9]+$ ]]; then
|
||||
echo "::error::VERSION_CODE_OFFSET from config.properties is not numeric: '$VERSION_CODE_OFFSET'"
|
||||
exit 1
|
||||
fi
|
||||
VERSION_CODE=$((COMMIT_COUNT + VERSION_CODE_OFFSET))
|
||||
echo "versionCode=$VERSION_CODE" >> "$GITHUB_OUTPUT"
|
||||
shell: bash
|
||||
|
||||
release-google:
|
||||
runs-on: ubuntu-26.04
|
||||
timeout-minutes: 90
|
||||
needs: [prepare-build-info]
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ inputs.tag_name }}
|
||||
fetch-depth: 0
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Gradle Setup
|
||||
uses: ./.github/actions/gradle-setup
|
||||
@@ -154,7 +95,6 @@ jobs:
|
||||
DATADOG_APPLICATION_ID: ${{ secrets.DATADOG_APPLICATION_ID }}
|
||||
DATADOG_CLIENT_TOKEN: ${{ secrets.DATADOG_CLIENT_TOKEN }}
|
||||
GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }}
|
||||
GOOGLE_PLAY_JSON_KEY: ${{ secrets.GOOGLE_PLAY_JSON_KEY }}
|
||||
run: |
|
||||
rm -f ./androidApp/google-services.json
|
||||
echo "$GSERVICES" > ./androidApp/google-services.json
|
||||
@@ -165,19 +105,18 @@ jobs:
|
||||
echo "datadogClientToken=$DATADOG_CLIENT_TOKEN"
|
||||
echo "MAPS_API_KEY=$GOOGLE_MAPS_API_KEY"
|
||||
} >> ./secrets.properties
|
||||
echo "$GOOGLE_PLAY_JSON_KEY" > ./fastlane/play-store-credentials.json
|
||||
|
||||
- name: Setup Fastlane
|
||||
uses: ruby/setup-ruby@14594264cd68ce8a2345dd349bc3d138a4ef85c8 # v1.327.0
|
||||
with:
|
||||
ruby-version: '4.0.7'
|
||||
bundler-cache: true
|
||||
|
||||
- name: Build and Deploy Google Play to Internal Track with Fastlane
|
||||
# Build only: publish-play uploads the bundle once every leg has built.
|
||||
- name: Build the Google release
|
||||
env:
|
||||
VERSION_NAME: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}
|
||||
VERSION_CODE: ${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }}
|
||||
run: bundle exec fastlane internal
|
||||
VERSION_NAME: ${{ inputs.version_name }}
|
||||
VERSION_CODE: ${{ inputs.version_code }}
|
||||
run: >
|
||||
./gradlew :androidApp:bundleGoogleRelease :androidApp:assembleGoogleRelease
|
||||
-Pandroid.injected.version.name="$VERSION_NAME"
|
||||
-Pandroid.injected.version.code="$VERSION_CODE"
|
||||
-PaboutLibraries.release=true
|
||||
-Pmeshtastic.disableAbiSplits=true
|
||||
|
||||
- name: List outputs
|
||||
run: ls -R androidApp/build/outputs/
|
||||
@@ -212,15 +151,13 @@ jobs:
|
||||
|
||||
release-fdroid:
|
||||
runs-on: ubuntu-26.04
|
||||
timeout-minutes: 90
|
||||
needs: [prepare-build-info]
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ inputs.tag_name }}
|
||||
fetch-depth: 0
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Gradle Setup
|
||||
uses: ./.github/actions/gradle-setup
|
||||
@@ -238,17 +175,15 @@ jobs:
|
||||
echo "$KEYSTORE" | base64 -di > "./androidApp/$KEYSTORE_FILENAME"
|
||||
echo "$KEYSTORE_PROPERTIES" > ./keystore.properties
|
||||
|
||||
- name: Setup Fastlane
|
||||
uses: ruby/setup-ruby@14594264cd68ce8a2345dd349bc3d138a4ef85c8 # v1.327.0
|
||||
with:
|
||||
ruby-version: '4.0.7'
|
||||
bundler-cache: true
|
||||
|
||||
- name: Build F-Droid with Fastlane
|
||||
# No aboutLibraries.release: offlineMode keeps the output matching F-Droid's reproducible rebuild.
|
||||
- name: Build the F-Droid release
|
||||
env:
|
||||
VERSION_NAME: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}
|
||||
VERSION_CODE: ${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }}
|
||||
run: bundle exec fastlane fdroid_build
|
||||
VERSION_NAME: ${{ inputs.version_name }}
|
||||
VERSION_CODE: ${{ inputs.version_code }}
|
||||
run: >
|
||||
./gradlew :androidApp:assembleFdroidRelease
|
||||
-Pandroid.injected.version.name="$VERSION_NAME"
|
||||
-Pandroid.injected.version.code="$VERSION_CODE"
|
||||
|
||||
- name: List outputs
|
||||
run: ls -R androidApp/build/outputs/
|
||||
@@ -268,10 +203,8 @@ jobs:
|
||||
subject-path: androidApp/build/outputs/apk/fdroid/release/*.apk
|
||||
|
||||
release-desktop:
|
||||
if: ${{ inputs.build_desktop }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 90
|
||||
needs: [prepare-build-info]
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -285,7 +218,6 @@ jobs:
|
||||
with:
|
||||
ref: ${{ inputs.tag_name }}
|
||||
fetch-depth: 0
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Gradle Setup
|
||||
uses: ./.github/actions/gradle-setup
|
||||
@@ -301,8 +233,8 @@ jobs:
|
||||
|
||||
- name: Package Native Distributions
|
||||
env:
|
||||
ORG_GRADLE_PROJECT_appVersionName: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}
|
||||
VERSION_CODE: ${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }}
|
||||
ORG_GRADLE_PROJECT_appVersionName: ${{ inputs.version_name }}
|
||||
VERSION_CODE: ${{ inputs.version_code }}
|
||||
APPIMAGE_EXTRACT_AND_RUN: 1
|
||||
SIGN_MACOS: ${{ runner.os == 'macOS' && secrets.APPLE_SIGNING_IDENTITY != '' && 'true' || 'false' }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
@@ -366,7 +298,7 @@ jobs:
|
||||
- name: Build AppImage from jpackage app-image
|
||||
if: runner.os == 'Linux'
|
||||
env:
|
||||
APP_VERSION_NAME: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}
|
||||
APP_VERSION_NAME: ${{ inputs.version_name }}
|
||||
run: scripts/build-appimage.sh
|
||||
|
||||
- name: List Desktop Binaries
|
||||
@@ -403,10 +335,8 @@ jobs:
|
||||
desktopApp/build/compose/jars/*-release.jar
|
||||
|
||||
create-flatpak-src:
|
||||
if: ${{ inputs.build_flatpak_src }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 60
|
||||
needs: [prepare-build-info]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -419,7 +349,6 @@ jobs:
|
||||
with:
|
||||
ref: ${{ inputs.tag_name }}
|
||||
fetch-depth: 0
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Gradle Setup
|
||||
uses: ./.github/actions/gradle-setup
|
||||
@@ -457,9 +386,8 @@ jobs:
|
||||
retention-days: 1
|
||||
|
||||
release-flatpak-src:
|
||||
if: ${{ inputs.build_flatpak_src }}
|
||||
runs-on: ubuntu-26.04
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-slim
|
||||
timeout-minutes: 10
|
||||
needs: [create-flatpak-src]
|
||||
steps:
|
||||
- name: Download Flatpak source artifacts
|
||||
@@ -501,96 +429,55 @@ jobs:
|
||||
# (the caller deletes the tag on a failed run): the capture runs soft, github-release
|
||||
# does not gate on it, and packaging falls back to the committed sets.
|
||||
store-screenshots:
|
||||
needs: [prepare-build-info]
|
||||
uses: ./.github/workflows/store-screenshots.yml
|
||||
with:
|
||||
ref: ${{ inputs.tag_name }}
|
||||
soft: true
|
||||
secrets: inherit
|
||||
|
||||
# One zip per flavor laid out as fastlane's images/ folder (google feeds the Play
|
||||
# listing, fdroid the committed tree F-Droid reads) and the five desktop PNGs loose,
|
||||
# so metainfo.xml can point at them by name. A flavor or the desktop set with a shot
|
||||
# missing is replaced whole by the committed one, never mixed.
|
||||
store-screenshots-assets:
|
||||
if: ${{ !cancelled() }}
|
||||
# Play receives the bundle only after every leg has built: a failed leg makes the caller
|
||||
# delete the tag, and Play keeps any versionCode it has been sent.
|
||||
publish-play:
|
||||
runs-on: ubuntu-26.04-arm
|
||||
timeout-minutes: 10
|
||||
continue-on-error: true
|
||||
needs: [prepare-build-info, store-screenshots]
|
||||
timeout-minutes: 15
|
||||
needs: [release-google, release-fdroid, release-desktop, release-flatpak-src]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ inputs.tag_name }}
|
||||
|
||||
- name: Download the captures
|
||||
continue-on-error: true
|
||||
- name: Download the Google AAB
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
with:
|
||||
pattern: store-screenshots-*
|
||||
path: shots
|
||||
name: google-aab
|
||||
path: ${{ runner.temp }}/google-aab
|
||||
|
||||
- name: Package the screenshots
|
||||
env:
|
||||
VERSION_CODE: ${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }}
|
||||
run: |
|
||||
mkdir -p out
|
||||
echo "### Store screenshots" >> "$GITHUB_STEP_SUMMARY"
|
||||
# Three form factors times five shots.
|
||||
for flavor in google fdroid; do
|
||||
src="shots/store-screenshots-$flavor"
|
||||
count=$(find "$src" -name '*.png' 2>/dev/null | wc -l)
|
||||
if [ "$count" -ne 15 ]; then
|
||||
echo "::warning::$flavor captured $count of 15 shots; the committed set is attached instead."
|
||||
echo "- $flavor: $count of 15 captured, committed set attached" >> "$GITHUB_STEP_SUMMARY"
|
||||
src=fastlane/metadata/android/en-US/images
|
||||
else
|
||||
echo "- $flavor: captured from the tag" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
(cd "$src" && zip -qr "$GITHUB_WORKSPACE/out/store-listing-screenshots-$flavor-${VERSION_CODE}.zip" .)
|
||||
done
|
||||
src=shots/store-screenshots-desktop
|
||||
count=$(find "$src" -name 'meshtastic-desktop-*.png' 2>/dev/null | wc -l)
|
||||
if [ "$count" -ne 5 ]; then
|
||||
echo "::warning::desktop captured $count of 5 shots; the committed set is attached instead."
|
||||
echo "- desktop: $count of 5 captured, committed set attached" >> "$GITHUB_STEP_SUMMARY"
|
||||
src=desktopApp/packaging/linux/screenshots
|
||||
else
|
||||
echo "- desktop: captured from the tag" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
cp "$src"/meshtastic-desktop-*.png out/
|
||||
|
||||
- name: Upload store screenshots artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@14594264cd68ce8a2345dd349bc3d138a4ef85c8 # v1.327.0
|
||||
with:
|
||||
name: store-screenshots
|
||||
path: out/*
|
||||
retention-days: 1
|
||||
bundler-cache: true
|
||||
|
||||
- name: Decode Play Store credentials
|
||||
env:
|
||||
GOOGLE_PLAY_JSON_KEY: ${{ secrets.GOOGLE_PLAY_JSON_KEY }}
|
||||
run: printf '%s\n' "$GOOGLE_PLAY_JSON_KEY" > fastlane/play-store-credentials.json
|
||||
|
||||
- name: Upload to the internal track
|
||||
env:
|
||||
AAB: ${{ runner.temp }}/google-aab/androidApp-google-release.aab
|
||||
run: bundle exec fastlane upload_internal aab:"$AAB"
|
||||
|
||||
- name: Clean up credentials
|
||||
if: always()
|
||||
run: rm -f fastlane/play-store-credentials.json
|
||||
|
||||
# Gates on the build jobs by name: store-screenshots-assets is in needs only so its
|
||||
# artifact exists before the download below, and its result is not consulted.
|
||||
github-release:
|
||||
if: >-
|
||||
${{ !cancelled()
|
||||
&& needs.prepare-build-info.result == 'success'
|
||||
&& needs.release-google.result == 'success'
|
||||
&& needs.release-fdroid.result == 'success'
|
||||
&& (needs.release-desktop.result == 'success' || !inputs.build_desktop)
|
||||
&& (needs.release-flatpak-src.result == 'success' || !inputs.build_flatpak_src) }}
|
||||
runs-on: ubuntu-26.04-arm
|
||||
timeout-minutes: 15
|
||||
needs:
|
||||
- prepare-build-info
|
||||
- release-google
|
||||
- release-fdroid
|
||||
- release-desktop
|
||||
- release-flatpak-src
|
||||
- store-screenshots-assets
|
||||
needs: [publish-play]
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -602,7 +489,7 @@ jobs:
|
||||
with:
|
||||
path: ./artifacts
|
||||
|
||||
# The raw captures are packaged by store-screenshots-assets; only its artifact ships.
|
||||
# Raw captures already uploaded stay out: store-screenshots-assets packages and attaches them.
|
||||
- name: Exclude intermediate artifacts from release
|
||||
run: rm -rf ./artifacts/flatpak-multisrc-* ./artifacts/store-screenshots-*
|
||||
|
||||
@@ -617,7 +504,7 @@ jobs:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
TAG: ${{ inputs.tag_name }}
|
||||
TARGET: ${{ inputs.commit_sha || github.sha }}
|
||||
TARGET: ${{ github.sha }}
|
||||
run: |
|
||||
# Fail the step if the listing itself fails — an empty PREV must only ever
|
||||
# mean "no published v* release exists yet", never a swallowed API error,
|
||||
@@ -651,9 +538,72 @@ jobs:
|
||||
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3
|
||||
with:
|
||||
tag_name: ${{ inputs.tag_name }}
|
||||
target_commitish: ${{ inputs.commit_sha || github.sha }}
|
||||
name: ${{ inputs.tag_name }} (${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }})
|
||||
target_commitish: ${{ github.sha }}
|
||||
name: ${{ inputs.tag_name }} (${{ inputs.version_code }})
|
||||
body_path: release-notes.md
|
||||
files: ./artifacts/**/*
|
||||
draft: true
|
||||
prerelease: true
|
||||
|
||||
# One zip per flavor laid out as fastlane's images/ folder (google feeds the Play
|
||||
# listing, fdroid the committed tree F-Droid reads) and the five desktop PNGs loose,
|
||||
# so metainfo.xml can point at them by name. A flavor or the desktop set with a shot
|
||||
# missing is replaced whole by the committed one, never mixed. They are attached to
|
||||
# the draft github-release created, so the draft never waits on the emulator.
|
||||
store-screenshots-assets:
|
||||
if: ${{ !cancelled() && needs.github-release.result == 'success' }}
|
||||
runs-on: ubuntu-slim
|
||||
timeout-minutes: 10
|
||||
continue-on-error: true
|
||||
needs: [store-screenshots, github-release]
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ inputs.tag_name }}
|
||||
|
||||
- name: Download the captures
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
with:
|
||||
pattern: store-screenshots-*
|
||||
path: shots
|
||||
|
||||
- name: Package the screenshots
|
||||
env:
|
||||
VERSION_CODE: ${{ inputs.version_code }}
|
||||
run: |
|
||||
mkdir -p out
|
||||
echo "### Store screenshots" >> "$GITHUB_STEP_SUMMARY"
|
||||
# Three form factors times five shots.
|
||||
for flavor in google fdroid; do
|
||||
src="shots/store-screenshots-$flavor"
|
||||
count=$(find "$src" -name '*.png' 2>/dev/null | wc -l)
|
||||
if [ "$count" -ne 15 ]; then
|
||||
echo "::warning::$flavor captured $count of 15 shots; the committed set is attached instead."
|
||||
echo "- $flavor: $count of 15 captured, committed set attached" >> "$GITHUB_STEP_SUMMARY"
|
||||
src=fastlane/metadata/android/en-US/images
|
||||
else
|
||||
echo "- $flavor: captured from the tag" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
(cd "$src" && zip -qr "$GITHUB_WORKSPACE/out/store-listing-screenshots-$flavor-${VERSION_CODE}.zip" .)
|
||||
done
|
||||
src=shots/store-screenshots-desktop
|
||||
count=$(find "$src" -name 'meshtastic-desktop-*.png' 2>/dev/null | wc -l)
|
||||
if [ "$count" -ne 5 ]; then
|
||||
echo "::warning::desktop captured $count of 5 shots; the committed set is attached instead."
|
||||
echo "- desktop: $count of 5 captured, committed set attached" >> "$GITHUB_STEP_SUMMARY"
|
||||
src=desktopApp/packaging/linux/screenshots
|
||||
else
|
||||
echo "- desktop: captured from the tag" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
cp "$src"/meshtastic-desktop-*.png out/
|
||||
|
||||
- name: Attach the screenshots to the draft release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
TAG: ${{ inputs.tag_name }}
|
||||
run: gh release upload "$TAG" out/* --clobber --repo "$REPO"
|
||||
@@ -1,10 +1,10 @@
|
||||
name: Store Screenshots
|
||||
|
||||
# Captures the store-listing screenshots from the real debug apps, connected to Demo
|
||||
# Mode's hidden showcase mesh. Android runs `:store-screenshots` on an emulator: the
|
||||
# google flavor feeds the Play listing, the fdroid flavor the fastlane tree F-Droid and
|
||||
# IzzyOnDroid read, each uploaded as `store-screenshots-<flavor>` laid out as
|
||||
# `images/<type>Screenshots/<n>_<name>.png`. Desktop runs the real app on a virtual
|
||||
# Mode's hidden showcase mesh. Android runs `:store-screenshots` for both flavors on one
|
||||
# emulator: the google flavor feeds the Play listing, the fdroid flavor the fastlane
|
||||
# tree F-Droid and IzzyOnDroid read, each uploaded as `store-screenshots-<flavor>` laid
|
||||
# out as `images/<type>Screenshots/<n>_<name>.png`. Desktop runs the real app on a virtual
|
||||
# display (`store-screenshots/capture-desktop.sh`) and uploads the five Flathub PNGs as
|
||||
# `store-screenshots-desktop`.
|
||||
#
|
||||
@@ -45,26 +45,17 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
android:
|
||||
name: Android (${{ matrix.flavor }})
|
||||
name: Android
|
||||
# Hosted x64 runners expose KVM; the emulator needs it.
|
||||
runs-on: ubuntu-26.04
|
||||
timeout-minutes: 60
|
||||
timeout-minutes: 35
|
||||
continue-on-error: ${{ inputs.soft == true }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- flavor: google
|
||||
task: Google
|
||||
- flavor: fdroid
|
||||
task: Fdroid
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.sha }}
|
||||
fetch-depth: 0
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Gradle Setup
|
||||
uses: ./.github/actions/gradle-setup
|
||||
@@ -76,7 +67,6 @@ jobs:
|
||||
# The google flavor draws Google Maps, which needs the debug Maps key; the fdroid
|
||||
# flavor draws MapLibre and needs nothing.
|
||||
- name: Provide the debug Maps key
|
||||
if: ${{ matrix.flavor == 'google' }}
|
||||
env:
|
||||
GOOGLE_MAPS_API_KEY_DEBUG: ${{ secrets.GOOGLE_MAPS_API_KEY_DEBUG }}
|
||||
run: |
|
||||
@@ -88,7 +78,9 @@ jobs:
|
||||
|
||||
# Built before the emulator boots, so the emulator step only installs and runs.
|
||||
- name: Build the app and the capture module
|
||||
run: ./gradlew :androidApp:assemble${{ matrix.task }}Debug :store-screenshots:assemble${{ matrix.task }}Debug
|
||||
run: >
|
||||
./gradlew :androidApp:assembleGoogleDebug :store-screenshots:assembleGoogleDebug
|
||||
:androidApp:assembleFdroidDebug :store-screenshots:assembleFdroidDebug
|
||||
|
||||
- name: Enable KVM (for the emulator)
|
||||
run: |
|
||||
@@ -97,10 +89,12 @@ jobs:
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger --name-match=kvm
|
||||
|
||||
# The runner's `script:` runs each line in its own shell. The capture copies each
|
||||
# PNG to /data/local/tmp, which outlives the test app's uninstall at the end of the
|
||||
# connected run. --no-configuration-cache: connected tasks in a com.android.test
|
||||
# module are not configuration-cache serializable (see scheduled-baseline.yml).
|
||||
# The runner's `script:` runs each line in its own shell and stops at the first that
|
||||
# fails, so a failure is recorded in out/failed and both flavors are still pulled.
|
||||
# Each flavor leaves its PNGs in /data/local/tmp/store-screenshots/<flavor>, which
|
||||
# outlives the test app's uninstall at the end of the connected run.
|
||||
# --no-configuration-cache: connected tasks in a com.android.test module are not
|
||||
# configuration-cache serializable (see scheduled-baseline.yml).
|
||||
- name: Capture on the emulator
|
||||
uses: reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2
|
||||
with:
|
||||
@@ -108,32 +102,45 @@ jobs:
|
||||
target: google_apis
|
||||
arch: x86_64
|
||||
profile: pixel_6
|
||||
cores: 4
|
||||
disable-animations: true
|
||||
emulator-options: -no-window -gpu swiftshader -noaudio -no-boot-anim -camera-back none
|
||||
script: |
|
||||
./gradlew :store-screenshots:connected${{ matrix.task }}DebugAndroidTest -Dorg.gradle.isolated-projects=false --no-configuration-cache
|
||||
mkdir -p out/images && adb pull /data/local/tmp/store-screenshots/. out/images/
|
||||
mkdir -p out/google out/fdroid
|
||||
./gradlew --continue :store-screenshots:connectedGoogleDebugAndroidTest :store-screenshots:connectedFdroidDebugAndroidTest -Dorg.gradle.isolated-projects=false --no-configuration-cache || touch out/failed
|
||||
adb pull /data/local/tmp/store-screenshots/google/. out/google/ || touch out/failed
|
||||
adb pull /data/local/tmp/store-screenshots/fdroid/. out/fdroid/ || touch out/failed
|
||||
test ! -e out/failed
|
||||
|
||||
- name: Summarize the captures
|
||||
if: ${{ always() }}
|
||||
env:
|
||||
FLAVOR: ${{ matrix.flavor }}
|
||||
run: |
|
||||
{
|
||||
echo "### Store screenshots (${FLAVOR})"
|
||||
if [ -d out/images ]; then
|
||||
find out/images -name '*.png' | sort | sed 's|^out/images/|- |'
|
||||
else
|
||||
echo "- none captured"
|
||||
fi
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
for flavor in google fdroid; do
|
||||
{
|
||||
echo "### Store screenshots (${flavor})"
|
||||
if [ -n "$(find "out/$flavor" -name '*.png' 2>/dev/null)" ]; then
|
||||
find "out/$flavor" -name '*.png' | sort | sed "s|^out/$flavor/|- |"
|
||||
else
|
||||
echo "- none captured"
|
||||
fi
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
done
|
||||
|
||||
- name: Upload the captures
|
||||
- name: Upload the google captures
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: store-screenshots-${{ matrix.flavor }}
|
||||
path: out/images
|
||||
name: store-screenshots-google
|
||||
path: out/google
|
||||
if-no-files-found: warn
|
||||
retention-days: 7
|
||||
|
||||
- name: Upload the fdroid captures
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: store-screenshots-fdroid
|
||||
path: out/fdroid
|
||||
if-no-files-found: warn
|
||||
retention-days: 7
|
||||
|
||||
@@ -141,7 +148,7 @@ jobs:
|
||||
desktop:
|
||||
name: Desktop
|
||||
runs-on: ubuntu-26.04
|
||||
timeout-minutes: 45
|
||||
timeout-minutes: 20
|
||||
continue-on-error: ${{ inputs.soft == true }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -149,7 +156,6 @@ jobs:
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.sha }}
|
||||
fetch-depth: 0
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Gradle Setup
|
||||
uses: ./.github/actions/gradle-setup
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
name: Update Changelog
|
||||
|
||||
# Manual only: promote.yml stamps the released section at release time on its
|
||||
# own. Dispatch this to refresh the [Unreleased] section between releases.
|
||||
# Create or Promote Release dispatches this after every internal, closed and open run;
|
||||
# a production promotion stamps the released section itself. Dispatch it by hand to
|
||||
# refresh the [Unreleased] section at any other time.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
# The PR is opened and queued with CROWDIN_GITHUB_TOKEN through .github/actions/bot-pr.
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
statuses: write
|
||||
|
||||
concurrency:
|
||||
group: changelog-${{ github.ref }}
|
||||
@@ -266,41 +266,13 @@ jobs:
|
||||
|
||||
- name: Create or update changelog PR
|
||||
if: steps.tags.outputs.prod != '' && steps.update.outputs.changed == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
BRANCH="automation/update-changelog"
|
||||
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# Force-update the automation branch
|
||||
git checkout -B "$BRANCH"
|
||||
git add CHANGELOG.md
|
||||
git commit -m "docs: update CHANGELOG.md"
|
||||
git push origin "$BRANCH" --force
|
||||
|
||||
# Create or update the PR
|
||||
EXISTING_PR=$(gh pr list --head "$BRANCH" --state open --json number -q '.[0].number')
|
||||
if [ -n "$EXISTING_PR" ]; then
|
||||
echo "Updated existing PR #$EXISTING_PR"
|
||||
else
|
||||
gh pr create \
|
||||
--title "docs: update CHANGELOG.md" \
|
||||
--body "Automated changelog refresh, dispatched from main." \
|
||||
--head "$BRANCH" \
|
||||
--base main \
|
||||
--label "automation" \
|
||||
--label "skip-changelog"
|
||||
echo "Created new changelog PR"
|
||||
fi
|
||||
|
||||
# Post the required "Check Workflow Status" commit status so the PR
|
||||
# isn't blocked. PRs from GITHUB_TOKEN don't trigger pull_request
|
||||
# workflows, so the normal CI never runs. CHANGELOG-only PRs don't
|
||||
# need CI checks.
|
||||
COMMIT_SHA=$(git rev-parse HEAD)
|
||||
gh api "repos/${{ github.repository }}/statuses/${COMMIT_SHA}" \
|
||||
-f state="success" \
|
||||
-f context="Check Workflow Status" \
|
||||
-f description="Skipped — changelog-only PR"
|
||||
uses: ./.github/actions/bot-pr
|
||||
with:
|
||||
token: ${{ secrets.CROWDIN_GITHUB_TOKEN }}
|
||||
branch: automation/update-changelog
|
||||
title: 'docs: update CHANGELOG.md'
|
||||
body: 'Automated changelog refresh, dispatched from main.'
|
||||
add-paths: CHANGELOG.md
|
||||
labels: |
|
||||
automation
|
||||
skip-changelog
|
||||
@@ -8,29 +8,17 @@ on:
|
||||
paths:
|
||||
- 'scripts/verify-flatpak/**'
|
||||
- '.github/workflows/verify-flatpak.yml'
|
||||
# The dependency surface the manifest captures is verified post-merge. This is not a
|
||||
# required check and never ran in the merge queue, so per-PR it cost ~2 runner slots
|
||||
# for a signal that blocks nothing; post-merge still catches a break within one merge,
|
||||
# and `main` itself was previously never verified at all.
|
||||
# Post-merge only for what the check itself depends on. The offline manifest pins the
|
||||
# Gradle distribution apart from the wrapper, so a wrapper bump is checked on merge.
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- 'scripts/verify-flatpak/**'
|
||||
- '.github/workflows/verify-flatpak.yml'
|
||||
- 'build.gradle.kts'
|
||||
- 'settings.gradle.kts'
|
||||
# The desktop module's build config shapes the uber jar the flatpak wraps.
|
||||
- 'desktopApp/**'
|
||||
# The offline manifest pins the Gradle distribution independently of the wrapper —
|
||||
# a wrapper bump without a manifest update breaks the offline build silently.
|
||||
- 'gradle/wrapper/**'
|
||||
# build.gradle.kts reads compose-multiplatform from the catalog (#6911), so a
|
||||
# catalog-only bump changes the manifest's platform URLs. Deliberately the whole
|
||||
# file and not a key filter: a filter naming today's keys goes stale silently the
|
||||
# moment the manifest reads another one — the failure #6911 existed to remove.
|
||||
- 'gradle/libs.versions.toml'
|
||||
# Drift no path filter can see: a Flathub runtime bump, or an upstream artifact that
|
||||
# moved or vanished. Nothing in this repo changes, so no other trigger would fire.
|
||||
# The dependency surface the manifest captures (desktopApp/**, the version catalog, the
|
||||
# root build scripts) is verified nightly, as is drift no path filter can see: a
|
||||
# Flathub runtime bump, or an upstream artifact that moved or vanished.
|
||||
schedule:
|
||||
- cron: '0 4 * * *'
|
||||
workflow_dispatch:
|
||||
@@ -60,8 +48,6 @@ jobs:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
# Renovate mirrors wrapper bumps into the manifest's distribution URL but cannot
|
||||
# rewrite its sha256, so on a wrapper bump this drift is expected: fail here in
|
||||
|
||||
@@ -17,8 +17,7 @@ on:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
# The PR is opened and queued with CROWDIN_GITHUB_TOKEN, as scheduled-updates.yml does:
|
||||
# GITHUB_TOKEN may not enable auto-merge on protected main, and its PRs run no checks.
|
||||
# The PR is opened and queued with CROWDIN_GITHUB_TOKEN through .github/actions/bot-pr.
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
@@ -31,7 +30,7 @@ concurrency:
|
||||
jobs:
|
||||
bump:
|
||||
if: ${{ github.repository == 'meshtastic/Meshtastic-Android' && !github.event.release.prerelease && !github.event.release.draft }}
|
||||
runs-on: ubuntu-26.04-arm
|
||||
runs-on: ubuntu-slim
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
TAG: ${{ inputs.tag || github.event.release.tag_name }}
|
||||
@@ -87,20 +86,16 @@ jobs:
|
||||
# Creates the PR, or updates it on a retry. Pushed with the PAT, so pull-request.yml's
|
||||
# AppStream and what's-new gates run on it like any other bump.
|
||||
- name: Open the bump PR
|
||||
id: pr
|
||||
if: ${{ steps.next.outputs.skip == 'false' }}
|
||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8
|
||||
uses: ./.github/actions/bot-pr
|
||||
with:
|
||||
token: ${{ secrets.CROWDIN_GITHUB_TOKEN }}
|
||||
commit-message: 'chore: bump VERSION_NAME_BASE to ${{ steps.next.outputs.next }}'
|
||||
title: 'chore: bump VERSION_NAME_BASE to ${{ steps.next.outputs.next }}'
|
||||
body: |
|
||||
Opens the ${{ steps.next.outputs.next }} line now that v${{ steps.next.outputs.shipped }} has shipped.
|
||||
|
||||
The metainfo `<description>` for ${{ steps.next.outputs.next }} is a placeholder. Rewrite it and re-run `python3 scripts/sync-play-changelog.py` before the ${{ steps.next.outputs.next }} internal cut, or it ships as the release Highlights and the Play what's-new.
|
||||
branch: ${{ steps.next.outputs.branch }}
|
||||
base: main
|
||||
delete-branch: true
|
||||
add-paths: |
|
||||
config.properties
|
||||
desktopApp/packaging/linux/org.meshtastic.MeshtasticDesktop.metainfo.xml
|
||||
@@ -108,23 +103,3 @@ jobs:
|
||||
labels: |
|
||||
automation
|
||||
skip-changelog
|
||||
|
||||
# Same request as scheduled-updates.yml: the queue sets the merge method, and a PR
|
||||
# that is already mergeable is refused the request and merges directly.
|
||||
- name: Enable auto-merge
|
||||
if: ${{ steps.pr.outputs.pull-request-operation == 'created' || steps.pr.outputs.pull-request-operation == 'updated' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.CROWDIN_GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ steps.pr.outputs.pull-request-number }}
|
||||
run: |
|
||||
PR_ID=$(gh pr view "$PR_NUMBER" --json id --jq .id)
|
||||
QUERY=$(cat <<'GQL'
|
||||
mutation($id: ID!) {
|
||||
enablePullRequestAutoMerge(input: { pullRequestId: $id }) {
|
||||
pullRequest { number autoMergeRequest { enabledAt } }
|
||||
}
|
||||
}
|
||||
GQL
|
||||
)
|
||||
gh api graphql -f query="$QUERY" -F id="$PR_ID" \
|
||||
|| gh pr merge "$PR_NUMBER"
|
||||
+37
-14
@@ -8,7 +8,7 @@ The entire release process is managed by a single GitHub Action: **`Create or Pr
|
||||
|
||||
- **Trigger:** To start a new release or promote an existing one, a developer runs the workflow from the GitHub Actions tab.
|
||||
- **Inputs:** The workflow requires the following inputs:
|
||||
1. `base_version`: The base version number you are releasing (e.g., `2.8.0`).
|
||||
1. `base_version`: The base version number you are releasing (e.g., `2.8.0`). It must be `X.Y.Z` and equal `VERSION_NAME_BASE` in `config.properties` at the commit being released (`HEAD` for an internal cut, the promoted tag's commit for a promotion), or the run stops before any tag is pushed.
|
||||
2. `channel`: The release channel you are targeting (`internal`, `closed`, `open`, or `production`).
|
||||
3. `dry_run`: If `true`, calculates the tag but does not push it or start the release (default: `false`).
|
||||
4. `no_review_in_flight`: **Promotions only, and a hard gate.** Before promoting, check
|
||||
@@ -18,16 +18,18 @@ The entire release process is managed by a single GitHub Action: **`Create or Pr
|
||||
already in flight. Internal releases and dry runs are exempt (Play internal testing
|
||||
skips full review).
|
||||
- **Automation:** The workflow handles everything automatically:
|
||||
- **Generates Changelog:** Categorizes merged PRs by their labels (per `.github/release.yml`) into GitHub's auto-generated release notes. The internal draft's notes cover the PRs since the previous published pre-release; a production promotion rewrites them over the whole range since the previous production tag, with the metainfo `<description>` for the version as a Highlights section on top, and opens a PR folding the same notes into `CHANGELOG.md`. Between releases that file is only refreshed by dispatching the `Update Changelog` workflow by hand.
|
||||
- **Tags & Builds** *(internal releases)*: Pushes the incremental tag first — there is no lint/test gate in this workflow, that's the separate PR/CI pipeline — then builds the Android bundle/APK and Desktop installers from that tag; if the build fails, an automatic cleanup job deletes the tag so a retry starts clean. Promotions skip this entirely and retag the already-built artifact (see below).
|
||||
- **Deploys Android:** Uploads the build to the correct Google Play track and attaches artifacts (`.aab`/`.apk`) to a GitHub Release. Each promotion also uploads the Play "What's new" text for every locale from `fastlane/metadata/android/<locale>/changelogs/default.txt`, which `scripts/sync-play-changelog.py` renders from the metainfo `<description>` and Crowdin translates.
|
||||
- **Captures the store screenshots** *(internal releases)*: `store-screenshots.yml` runs the real debug apps from the tag, connected to Demo Mode's showcase mesh, on an emulator per flavor and on a virtual display for desktop, and attaches `store-listing-screenshots-google-<versionCode>.zip`, `store-listing-screenshots-fdroid-<versionCode>.zip` and the five desktop PNGs to the release. A set with a shot missing is replaced whole by the committed one, so the metainfo's screenshot URLs still resolve, and the job summary says which set went up. A failed capture never fails the release.
|
||||
- **Generates Changelog:** Categorizes merged PRs by their labels (per `.github/release.yml`) into GitHub's auto-generated release notes. The internal draft's notes cover the PRs since the previous published pre-release; a production promotion rewrites them over the whole range since the previous production tag, with the metainfo `<description>` for the version as a Highlights section on top, and opens a PR folding the same notes into `CHANGELOG.md`. Every internal, closed and open run ends by dispatching the `Update Changelog` workflow, which refreshes that file's `[Unreleased]` section through its own PR. Every PR the release automation opens (these two, the screenshot refresh and the version bump) goes through `.github/actions/bot-pr` with `CROWDIN_GITHUB_TOKEN`, so it runs the normal PR checks and merges itself through the queue once they pass.
|
||||
- **Tags & Builds** *(internal releases)*: Pushes the incremental tag first — there is no lint/test gate in this workflow, that's the separate PR/CI pipeline — then builds the Android bundle/APK and Desktop installers from that tag; if the build fails, an automatic cleanup job deletes the tag so a retry starts clean. Once `publish-play` has uploaded the bundle the tag stays, since Play keeps that versionCode; re-run the failed jobs instead. Promotions skip this entirely and retag the already-built artifact (see below).
|
||||
- **Deploys Android:** Uploads the build to the correct Google Play track and attaches artifacts (`.aab`/`.apk`) to a GitHub Release. An internal cut sends the bundle to Play only after every Android, desktop and Flatpak leg has built, so a failed leg and its deleted tag leave nothing on Play. Each promotion also uploads the Play "What's new" text for every locale from `fastlane/metadata/android/<locale>/changelogs/default.txt`, which `scripts/sync-play-changelog.py` renders from the metainfo `<description>` and Crowdin translates.
|
||||
- **Captures the store screenshots** *(internal releases)*: `store-screenshots.yml` runs the real debug apps from the tag, connected to Demo Mode's showcase mesh, on one emulator for both flavors and on a virtual display for desktop, and attaches `store-listing-screenshots-google-<versionCode>.zip`, `store-listing-screenshots-fdroid-<versionCode>.zip` and the five desktop PNGs to the draft once it exists, so the draft does not wait on the capture. A set with a shot missing is replaced whole by the committed one, so the metainfo's screenshot URLs still resolve, and the job summary says which set went up. A failed capture never fails the release.
|
||||
- **Publishes the Play listing:** every promotion runs the `play_listing` lane with the tag's text for every locale and the google-flavor screenshots, as a dry run on closed and open and for real on production, held as "changes not sent for review". Production also opens a self-merging PR that writes the fdroid-flavor screenshots back into `fastlane/`, which F-Droid and IzzyOnDroid read from git, and the desktop set into `desktopApp/packaging/linux/screenshots/`.
|
||||
- **Publishes docs:** Every promotion dispatches `docs-release.yml` on the new tag (the tag is created with `GITHUB_TOKEN`, so its tag trigger never fires on its own).
|
||||
- **Writes a checklist:** The promotion run's summary lists what it did, what it dispatched, and what is still done by hand.
|
||||
- **Refreshes the Obtainium table:** Every promotion dispatches `scheduled-updates.yml` once the release is published, so the Obtainium table in `README.md` follows it without waiting for the scheduled run.
|
||||
- **Writes a checklist:** The promotion run's summary lists what it did, what it dispatched, and what is still done by hand. winget, the Microsoft Store, Homebrew and Flathub each read "will skip: `<SECRET>` unset" when their secret is missing, since those workflows and jobs then submit nothing.
|
||||
- **Reruns:** Re-running a failed `update-github-release` job finds the release under its final tag when the first attempt already moved it, and updates the bot PRs in place; its workflow dispatches run again. The Discord announcement goes out whenever the release step succeeded, whatever failed after it.
|
||||
- **Deploys Desktop** *(internal releases)*: Builds native installers (DMG, MSI, EXE, DEB, RPM, AppImage) and Flatpak sources on a matrix of runners and attaches them to the GitHub Release.
|
||||
- **Changelog:** Both the GitHub Release notes and `CHANGELOG.md` are generated from merged PR labels, not raw commit messages — label PRs correctly (`enhancement`, `bugfix`, etc.) to keep them accurate.
|
||||
- **Not part of this workflow:** Firmware/hardware/device-links lists and Crowdin translations are kept current by a separate hourly workflow, `scheduled-updates.yml` ("Scheduled Updates (Firmware, Hardware, Translations)"), which opens its own PR rather than committing directly and enables auto-merge on it, so it lands through the merge queue once its checks pass — it never runs as part of a release. `VERSION_NAME_BASE` in `config.properties` moves to the next patch version after each production release: `promote.yml` dispatches `version-bump.yml`, which runs `scripts/bump-version-name.py` and opens a self-merging PR carrying the new `<release>` entry in `desktopApp/packaging/linux/org.meshtastic.MeshtasticDesktop.metainfo.xml`, its five `<image>` URLs moved to `releases/download/v<version>/`, and `fastlane/metadata/android/en-US/changelogs/default.txt` rendered from that entry. `pull-request.yml` fails a bump PR missing any of them; the bot PR is opened with `CROWDIN_GITHUB_TOKEN`, so those checks run on it and the merge queue takes it. The entry's paragraph is a placeholder; replace it and re-run `scripts/sync-play-changelog.py` before the next internal cut, because it becomes the release Highlights and Play's "What's new". A minor or major line is a hand PR running the same script, and the workflow skips when `main` is already past the shipped version. `Create or Promote Release` only *reads* `VERSION_NAME_BASE`/`VERSION_CODE_OFFSET` from `config.properties` to compute the build's version name/code.
|
||||
- **Not part of this workflow:** Firmware/hardware/device-links lists and Crowdin translations are kept current by a separate scheduled workflow, `scheduled-updates.yml` ("Scheduled Updates (Firmware, Hardware, Translations)"), which opens its own PR rather than committing directly and enables auto-merge on it, so it lands through the merge queue once its checks pass. A promotion dispatches it (above) without waiting on it. `VERSION_NAME_BASE` in `config.properties` moves to the next patch version after each production release: `promote.yml` dispatches `version-bump.yml`, which runs `scripts/bump-version-name.py` and opens a self-merging PR carrying the new `<release>` entry in `desktopApp/packaging/linux/org.meshtastic.MeshtasticDesktop.metainfo.xml`, its five `<image>` URLs moved to `releases/download/v<version>/`, and `fastlane/metadata/android/en-US/changelogs/default.txt` rendered from that entry. `pull-request.yml` fails a bump PR missing any of them; the bot PR is opened with `CROWDIN_GITHUB_TOKEN`, so those checks run on it and the merge queue takes it. The entry's paragraph is a placeholder; replace it and re-run `scripts/sync-play-changelog.py` before the next internal cut, because it becomes the release Highlights and Play's "What's new". A minor or major line is a hand PR running the same script, and the workflow skips when `main` is already past the shipped version. `Create or Promote Release` only *reads* `config.properties` at the commit being released: `VERSION_NAME_BASE` must match `base_version`, and `VERSION_CODE_OFFSET` plus that commit's count gives the build's version code.
|
||||
|
||||
## Release Steps
|
||||
|
||||
@@ -42,9 +44,9 @@ The entire release process is managed by a single GitHub Action: **`Create or Pr
|
||||
|
||||
The workflow will:
|
||||
1. **Tag** the current commit on the branch with an incremental internal tag (e.g., `v2.8.0-internal.1`) — no new commit is created; it tags whatever is already at `HEAD`.
|
||||
2. **Build & Deploy** the built Android artifact to the Play Store Internal track.
|
||||
3. **Build Desktop** native installers and Flatpak sources on macOS, Windows, and Linux runners.
|
||||
4. Publish a **draft** pre-release on GitHub with all artifacts attached. It stays a draft until
|
||||
2. **Build** the Android bundle and APKs, and the desktop installers and Flatpak sources on macOS, Windows, and Linux runners.
|
||||
3. **Deploy** the Android bundle to the Play Store Internal track once every build has succeeded.
|
||||
4. Publish a **draft** pre-release on GitHub with all artifacts attached; the store screenshots follow when the capture finishes. It stays a draft until
|
||||
the first promotion (closed/open/production), at which point `promote.yml` un-drafts the
|
||||
*same* release object (retagging it to the new channel's tag) rather than creating a new one.
|
||||
|
||||
@@ -70,18 +72,39 @@ After testing is complete on all pre-release channels, you can create the final
|
||||
Start from the promotion run's summary: it lists what the run did and dispatched, and what
|
||||
remains by hand.
|
||||
|
||||
1. **Verify Android:** Check the Google Play Console to ensure the build is available on the correct track. A production promotion starts a staged rollout; complete it in the console.
|
||||
1. **Verify Android:** Check the Google Play Console to ensure the build is available on the correct track. A production promotion starts a staged rollout at 10% (open at 50%); widen, complete or halt it with the **`Play Rollout`** workflow (see Staged Rollout below).
|
||||
2. **Verify Desktop:** Download and smoke-test at least one installer (DMG, MSI, or AppImage) from the GitHub Release.
|
||||
3. **Verify the desktop store submissions** *(production only — see below)*: the Microsoft Store
|
||||
submission in Partner Center, and the pull request opened against `microsoft/winget-pkgs`.
|
||||
Each store workflow warns in its summary when its secrets are not set and it submitted nothing.
|
||||
4. **Flathub** *(production only)*: bump the manifest in `flathub/org.meshtastic.MeshtasticDesktop` (see Flatpak below).
|
||||
Each store workflow warns in its summary when its secrets are not set and it submitted nothing,
|
||||
and the promotion checklist already says so.
|
||||
4. **Flathub** *(production only)*: merge the `update-flathub` PR in `flathub/org.meshtastic.MeshtasticDesktop` once Flathub's test build passes, or bump it by hand when `FLATHUB_TOKEN` is unset (see Flatpak below).
|
||||
5. **Post-Release Cleanup** *(production only)*: `Docs Release` dispatches `post-release-cleanup.yml` with `confirm_deletion: true` once it has published `/vX.Y.Z/`, deleting the pre-releases, tags and docs snapshots at or below `X.Y.Z`. Check that run; a manual dispatch is the retry and defaults to a dry run.
|
||||
6. **Next version line** *(production only)*: the `version-bump.yml` PR bumps `VERSION_NAME_BASE` and merges itself. Replace its placeholder `<description>` before the next internal cut.
|
||||
7. **Merge:** If a `release/*` branch was used for stabilization (CI runs the same PR checks
|
||||
against PRs targeting `release/**` as it does for `main`), merge it back into `main` now
|
||||
that production has shipped.
|
||||
|
||||
### Staged Rollout
|
||||
|
||||
**`Play Rollout`** (`play-rollout.yml`) changes the one `inProgress` release on the `production`
|
||||
or `beta` track. Its inputs are the `track`, an `action` and, for `rollout`, a `fraction`:
|
||||
|
||||
| Action | Effect |
|
||||
|---|---|
|
||||
| `rollout` | Widens the release to `fraction`, which must be above the current fraction and below 1 |
|
||||
| `complete` | Ships the release to every user |
|
||||
| `halt` | Stops the rollout at its current fraction |
|
||||
|
||||
It carries the same `no_review_in_flight` gate as a promotion, because a committed change
|
||||
cancels and restarts any review in flight. It runs in its own concurrency group, not
|
||||
`Create or Promote Release`'s, since a group keeps one pending run and a rollout queued there
|
||||
would cancel a pending promotion. The run stops without
|
||||
changing anything unless the track holds exactly one `inProgress` release, and it verifies the
|
||||
new status and fraction on the track afterwards. A halted release is resumed or completed in the
|
||||
Play Console, since `supply` only acts on `inProgress` releases. When Play will not send a change
|
||||
for review on its own, the change waits under Publishing overview in the Play Console.
|
||||
|
||||
### Desktop Store Publishing (production only)
|
||||
|
||||
Publishing a **production** release also fires two workflows, both keyed on the GitHub
|
||||
@@ -130,7 +153,7 @@ Desktop uses the same version resolution chain as Android — both read `VERSION
|
||||
|
||||
### Flatpak
|
||||
|
||||
Flatpak packaging is maintained externally at [flathub/org.meshtastic.MeshtasticDesktop](https://github.com/flathub/org.meshtastic.MeshtasticDesktop). It builds `:desktopApp:packageUberJarForCurrentOS` (not the native distribution pipeline) and handles JBR bundling; the AppStream metainfo and `.desktop` entry it installs come from this repo, out of the tag it builds. So the `<release>` notes ship with the tag, and the `<screenshot>` URLs name the desktop PNGs the internal cut attached to that version's release (`releases/download/v<version>/`), which Flathub's guidelines allow and a branch link would not. The Flathub bump is a hand-opened PR that moves four things together: the tag and commit, the Gradle distribution zip URL and sha256 (from the tag's `gradle/wrapper/gradle-wrapper.properties`), and the release's `flatpak-sources.json` asset. Every flathubbot zip-bump PR so far has failed its test build; close them rather than merge them. The offline-build sources it consumes are captured in-repo by `scripts/verify-flatpak/` (see its README).
|
||||
Flatpak packaging is maintained externally at [flathub/org.meshtastic.MeshtasticDesktop](https://github.com/flathub/org.meshtastic.MeshtasticDesktop). It builds `:desktopApp:packageUberJarForCurrentOS` (not the native distribution pipeline) and handles JBR bundling; the AppStream metainfo and `.desktop` entry it installs come from this repo, out of the tag it builds. So the `<release>` notes ship with the tag, and the `<screenshot>` URLs name the desktop PNGs the internal cut attached to that version's release (`releases/download/v<version>/`), which Flathub's guidelines allow and a branch link would not. A production promotion's `update-flathub` job opens the bump PR with `FLATHUB_TOKEN`, and skips with a notice when that secret is unset. The PR moves four things together: the tag and commit, the Gradle distribution zip URL and sha256 (from the tag's `gradle/wrapper/gradle-wrapper.properties`), and the release's `flatpak-sources.json` asset. `scripts/verify-flatpak/bump-flathub-manifest.py` rewrites those manifest fields in place and fails, leaving the bump to be done by hand, when any of them no longer matches exactly once. The JBR, the runtime and the patches are left alone; Flathub's test build on the PR checks them against the tag. flathubbot's zip-bump PRs follow the latest Gradle rather than the tag's wrapper and fail their test build; close them rather than merge them. The offline-build sources it consumes are captured in-repo by `scripts/verify-flatpak/` (see its README), and `verify-flatpak.yml` builds them offline nightly and on changes to that directory, the workflow or the Gradle wrapper.
|
||||
|
||||
## Build Attestations & Provenance
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ class AboutLibrariesConventionPlugin : Plugin<Project> {
|
||||
pluginManager.apply(libs.plugin("aboutlibraries").get().pluginId)
|
||||
|
||||
extensions.configure<AboutLibrariesExtension> {
|
||||
// aboutLibraries.release=true is only passed for google builds (see Fastfile).
|
||||
// release.yml passes aboutLibraries.release=true to the Google and desktop release builds only.
|
||||
// For fdroid/reproducible builds, offlineMode=true ensures no network calls
|
||||
// and deterministic output. See: https://github.com/meshtastic/Meshtastic-Android/issues/3231
|
||||
val isReleaseBuild =
|
||||
@@ -69,8 +69,7 @@ class AboutLibrariesConventionPlugin : Plugin<Project> {
|
||||
// See: https://github.com/meshtastic/Meshtastic-Android/issues/3231
|
||||
tasks
|
||||
.matching {
|
||||
it.name.startsWith("process") &&
|
||||
(it.name.endsWith("Resources") || it.name.endsWith("JavaRes"))
|
||||
it.name.startsWith("process") && (it.name.endsWith("Resources") || it.name.endsWith("JavaRes"))
|
||||
}
|
||||
.configureEach { dependsOn("exportLibraryDefinitions") }
|
||||
}
|
||||
|
||||
@@ -75,14 +75,14 @@ Rendering is host-deterministic here (layoutlib): a local `update` produces refe
|
||||
|
||||
The store-listing screenshots (Play, F-Droid, IzzyOnDroid, and the desktop app's Flathub listing) are taken from the real apps, connected to Demo Mode's hidden showcase mesh (`/connections?address=mshowcase`, `MockScenario.SHOWCASE` in `:core:network`), rather than drawn. Every screen is reached by its deep link, so the flow does not depend on the display language, and each shot is kept once the window has stopped changing.
|
||||
|
||||
- **Android: `:store-screenshots`**, a UiAutomator 2.4 test module targeting `:androidApp`. For each surface `fastlane supply` uploads it sets the display size and density, relaunches the debug app through its shell-only `AutomationLauncher` alias with `skip_onboarding` and `skip_connect_confirm`, and saves the five listing shots, full screen with a SystemUI demo-mode status bar, to `/data/local/tmp/store-screenshots` on the device.
|
||||
- **Android: `:store-screenshots`**, a UiAutomator 2.4 test module targeting `:androidApp`. For each surface `fastlane supply` uploads it sets the display size and density, relaunches the debug app through its shell-only `AutomationLauncher` alias with `skip_onboarding` and `skip_connect_confirm`, and saves the five listing shots, full screen with a SystemUI demo-mode status bar, to `/data/local/tmp/store-screenshots/<flavor>` on the device.
|
||||
- **Desktop: `store-screenshots/capture-desktop.sh`** runs the real desktop debug build on an Xvfb display, one launch per screen with that screen's deep link, and saves the five Flathub shots. The map needs Skiko's OpenGL renderer and Skiko refuses any GL adapter named `llvmpipe` or `virgl`, so Mesa runs GL through zink over lavapipe.
|
||||
|
||||
On an emulator or device, one flavor at a time:
|
||||
|
||||
```shell
|
||||
./gradlew :store-screenshots:connectedFdroidDebugAndroidTest
|
||||
adb pull /data/local/tmp/store-screenshots/. fastlane/metadata/android/en-US/images/
|
||||
adb pull /data/local/tmp/store-screenshots/fdroid/. fastlane/metadata/android/en-US/images/
|
||||
```
|
||||
|
||||
| Folder | Size | Window | Uploaded by |
|
||||
@@ -92,7 +92,7 @@ adb pull /data/local/tmp/store-screenshots/. fastlane/metadata/android/en-US/ima
|
||||
| `tenInchScreenshots/` | 2560×1440 @320 dpi | expanded: rail, list beside detail | `fastlane supply` |
|
||||
| `desktopApp/packaging/linux/screenshots/` | 1280×800 | expanded: rail, list beside detail | Flathub, through the release assets `metainfo.xml` names |
|
||||
|
||||
`.github/workflows/store-screenshots.yml` runs both on hosted runners, per flavor for Android (google for the Play listing, fdroid for the committed tree), on every internal release, on demand, and on pull requests that touch the renderer or the showcase mesh. The release pipeline attaches the captures to the release, publishes the Play listing from them on production, and opens a self-merging PR that writes the fdroid and desktop sets back here (`RELEASE_PROCESS.md`).
|
||||
`.github/workflows/store-screenshots.yml` runs both on hosted runners, with both Android flavors in one job on one emulator (google for the Play listing, fdroid for the committed tree), on every internal release, on demand, and on pull requests that touch the renderer or the showcase mesh. The release pipeline attaches the captures to the release, publishes the Play listing from them on production, and opens a self-merging PR that writes the fdroid and desktop sets back here (`RELEASE_PROCESS.md`).
|
||||
|
||||
### Baseline Profile / startup performance
|
||||
|
||||
|
||||
+27
-30
@@ -1,18 +1,19 @@
|
||||
# Lanes are invoked by the workflows in .github/workflows (release.yml,
|
||||
# promote.yml, play-listing.yml); `bundle exec fastlane lanes` lists them.
|
||||
# promote.yml, play-listing.yml, play-rollout.yml); `bundle exec fastlane lanes`
|
||||
# lists them.
|
||||
# Play credentials come from fastlane/play-store-credentials.json, written by
|
||||
# the workflow from the GOOGLE_PLAY_JSON_KEY secret and deleted afterwards.
|
||||
|
||||
default_platform(:android)
|
||||
|
||||
platform :android do
|
||||
desc "Deploy a new version to the internal track on Google Play"
|
||||
lane :internal do |options|
|
||||
aab_path = build_google_release
|
||||
desc "Upload a built Google release bundle to the internal track on Google Play. Pass aab:<path>"
|
||||
lane :upload_internal do |options|
|
||||
UI.user_error!("Pass the bundle to upload as aab:<path>") if options[:aab].to_s.empty?
|
||||
|
||||
upload_to_play_store(
|
||||
track: 'internal',
|
||||
aab: aab_path,
|
||||
aab: options[:aab],
|
||||
release_status: 'completed',
|
||||
skip_upload_apk: true,
|
||||
skip_upload_metadata: true,
|
||||
@@ -49,31 +50,27 @@ platform :android do
|
||||
)
|
||||
end
|
||||
|
||||
desc "Build the F-Droid release"
|
||||
lane :fdroid_build do
|
||||
gradle(
|
||||
task: "assembleFdroidRelease",
|
||||
properties: {
|
||||
"android.injected.version.name" => ENV['VERSION_NAME'],
|
||||
"android.injected.version.code" => ENV['VERSION_CODE'],
|
||||
# Intentionally omit aboutLibraries.release — fdroid builds must use
|
||||
# offlineMode so the output matches F-Droid's reproducible rebuild.
|
||||
}
|
||||
)
|
||||
end
|
||||
desc "Write every release on a Play track (status, version codes, user fraction) as JSON. Pass track:<name> out:<path>. Reads only; the edit is discarded"
|
||||
lane :play_track_releases do |options|
|
||||
UI.user_error!("Pass track:<name> and out:<path>") if options[:track].to_s.empty? || options[:out].to_s.empty?
|
||||
|
||||
desc "Build the Google Release"
|
||||
private_lane :build_google_release do
|
||||
gradle(
|
||||
task: "bundleGoogleRelease assembleGoogleRelease",
|
||||
print_command: false,
|
||||
properties: {
|
||||
"android.injected.version.name" => ENV['VERSION_NAME'],
|
||||
"android.injected.version.code" => ENV['VERSION_CODE'],
|
||||
"aboutLibraries.release" => "true",
|
||||
"meshtastic.disableAbiSplits" => "true"
|
||||
}
|
||||
)
|
||||
lane_context[SharedValues::GRADLE_AAB_OUTPUT_PATH]
|
||||
require 'json'
|
||||
require 'supply'
|
||||
require 'supply/options'
|
||||
require 'supply/reader'
|
||||
|
||||
# Lane code runs in ./fastlane, and the Appfile's key path is relative to the project root.
|
||||
Dir.chdir('..') do
|
||||
Supply.config = FastlaneCore::Configuration.create(Supply::Options.available_options, { track: options[:track].to_s })
|
||||
track = Supply::Reader.new.track_meta
|
||||
releases = (track&.releases || []).map do |release|
|
||||
{
|
||||
status: release.status,
|
||||
version_codes: (release.version_codes || []).map(&:to_i),
|
||||
user_fraction: release.user_fraction,
|
||||
}
|
||||
end
|
||||
File.write(options[:out], JSON.pretty_generate(releases))
|
||||
end
|
||||
end
|
||||
end
|
||||
+6
-6
@@ -15,13 +15,13 @@ For _fastlane_ installation instructions, see [Installing _fastlane_](https://do
|
||||
|
||||
## Android
|
||||
|
||||
### android internal
|
||||
### android upload_internal
|
||||
|
||||
```sh
|
||||
[bundle exec] fastlane android internal
|
||||
[bundle exec] fastlane android upload_internal
|
||||
```
|
||||
|
||||
Deploy a new version to the internal track on Google Play
|
||||
Upload a built Google release bundle to the internal track on Google Play. Pass aab:<path>
|
||||
|
||||
### android play_listing
|
||||
|
||||
@@ -31,13 +31,13 @@ Deploy a new version to the internal track on Google Play
|
||||
|
||||
Upload the store listing - title, descriptions, feature graphic, icon and screenshots - for every locale under fastlane/metadata/android. Touches no build or track. Dry-runs unless validate_only:false
|
||||
|
||||
### android fdroid_build
|
||||
### android play_track_releases
|
||||
|
||||
```sh
|
||||
[bundle exec] fastlane android fdroid_build
|
||||
[bundle exec] fastlane android play_track_releases
|
||||
```
|
||||
|
||||
Build the F-Droid release
|
||||
Write every release on a Play track (status, version codes, user fraction) as JSON. Pass track:<name> out:<path>. Reads only; the edit is discarded
|
||||
|
||||
----
|
||||
|
||||
|
||||
@@ -79,3 +79,6 @@ executing the Gradle build, or run the full script on a Linux host.
|
||||
- `desktop-offline.yaml` — patched manifest. Kept in sync manually with the upstream packaging;
|
||||
diff against `https://raw.githubusercontent.com/flathub/org.meshtastic.MeshtasticDesktop/master/org.meshtastic.MeshtasticDesktop.yaml`
|
||||
if upstream changes something material.
|
||||
- `bump-flathub-manifest.py` - points the upstream manifest at a release: the source tag and
|
||||
commit, and the Gradle zip the tag's wrapper pins. `promote.yml`'s `update-flathub` job runs it
|
||||
on every production promotion, then commits the release's `flatpak-sources.json` beside it.
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Point the Flathub manifest at a release: the source tag and commit, and the Gradle zip.
|
||||
|
||||
Usage: bump-flathub-manifest.py <manifest.yaml> <tag> <commit> <gradle-wrapper.properties>
|
||||
|
||||
The Gradle distribution URL and sha256 come from the tag's own wrapper properties, since the
|
||||
offline build can only run the Gradle version that tag pins. Each field is rewritten in
|
||||
place, so comments and layout survive. Any field that does not match exactly once fails the
|
||||
run instead of guessing, and the manifest is then bumped by hand.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
|
||||
GIT_SOURCE = re.compile(
|
||||
r"^(?P<lead>\s+url: https://github\.com/meshtastic/Meshtastic-Android\.git\n"
|
||||
r"\s+tag: )\S+(?P<mid>\n\s+commit: )[0-9a-f]{40}$",
|
||||
re.MULTILINE,
|
||||
)
|
||||
GRADLE_ZIP = re.compile(
|
||||
r"^(?P<lead>\s+url: )https://services\.gradle\.org/distributions/gradle-[^\s/]+\.zip"
|
||||
r"(?P<mid>\n\s+sha256: )[0-9a-f]{64}$",
|
||||
re.MULTILINE,
|
||||
)
|
||||
|
||||
|
||||
def wrapper_distribution(path: str) -> tuple[str, str]:
|
||||
props = {}
|
||||
with open(path, encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and not line.startswith("#") and "=" in line:
|
||||
key, value = line.split("=", 1)
|
||||
props[key.strip()] = value.strip().replace("\\:", ":")
|
||||
url = props.get("distributionUrl", "")
|
||||
sha = props.get("distributionSha256Sum", "")
|
||||
if not re.fullmatch(r"https://services\.gradle\.org/distributions/gradle-[^\s/]+\.zip", url):
|
||||
sys.exit(f"{path}: distributionUrl '{url}' is not a services.gradle.org distribution")
|
||||
if not re.fullmatch(r"[0-9a-f]{64}", sha):
|
||||
sys.exit(f"{path}: distributionSha256Sum '{sha}' is not a sha256")
|
||||
return url, sha
|
||||
|
||||
|
||||
def replace_once(pattern: re.Pattern, value_a: str, value_b: str, text: str, what: str) -> str:
|
||||
new, count = pattern.subn(lambda m: m["lead"] + value_a + m["mid"] + value_b, text)
|
||||
if count != 1:
|
||||
sys.exit(f"{what}: expected exactly one match in the manifest, found {count}")
|
||||
return new
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if len(sys.argv) != 5:
|
||||
sys.exit(__doc__)
|
||||
manifest, tag, commit, wrapper = sys.argv[1:]
|
||||
if not re.fullmatch(r"v\d+\.\d+\.\d+", tag):
|
||||
sys.exit(f"'{tag}' is not a production tag (vX.Y.Z)")
|
||||
if not re.fullmatch(r"[0-9a-f]{40}", commit):
|
||||
sys.exit(f"'{commit}' is not a full commit sha")
|
||||
url, sha = wrapper_distribution(wrapper)
|
||||
|
||||
with open(manifest, encoding="utf-8") as f:
|
||||
text = f.read()
|
||||
text = replace_once(GIT_SOURCE, tag, commit, text, "Meshtastic-Android git source (url, tag, commit)")
|
||||
text = replace_once(GRADLE_ZIP, url, sha, text, "Gradle distribution (url, sha256)")
|
||||
with open(manifest, "w", encoding="utf-8") as f:
|
||||
f.write(text)
|
||||
print(f"{manifest}: {tag} at {commit}, {url.rsplit('/', 1)[-1]}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -19,9 +19,10 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
// Captures the store-listing screenshots from the real debug app on a device or emulator:
|
||||
// ./gradlew :store-screenshots:connectedGoogleDebugAndroidTest (Play)
|
||||
// ./gradlew :store-screenshots:connectedFdroidDebugAndroidTest (F-Droid, IzzyOnDroid)
|
||||
// PNGs are left on the device in /data/local/tmp/store-screenshots, laid out like fastlane's images/; pull them with
|
||||
// adb pull /data/local/tmp/store-screenshots/. <dir>
|
||||
// .github/workflows/store-screenshots.yml does this on an emulator.
|
||||
// PNGs are left on the device in /data/local/tmp/store-screenshots/<flavor>, laid out like fastlane's images/; pull
|
||||
// them with
|
||||
// adb pull /data/local/tmp/store-screenshots/<flavor>/. <dir>
|
||||
// .github/workflows/store-screenshots.yml does this for both flavors on one emulator.
|
||||
plugins {
|
||||
alias(libs.plugins.android.test)
|
||||
alias(libs.plugins.meshtastic.detekt)
|
||||
@@ -52,14 +53,21 @@ android {
|
||||
create("google") {
|
||||
dimension = "marketplace"
|
||||
testInstrumentationRunnerArguments["targetAppId"] = "com.geeksville.mesh.google.debug"
|
||||
testInstrumentationRunnerArguments["flavor"] = "google"
|
||||
}
|
||||
create("fdroid") {
|
||||
dimension = "marketplace"
|
||||
testInstrumentationRunnerArguments["targetAppId"] = "com.geeksville.mesh.fdroid.debug"
|
||||
testInstrumentationRunnerArguments["flavor"] = "fdroid"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Both flavors can capture on one device in one invocation; each run resizes that device's display.
|
||||
tasks
|
||||
.named { it == "connectedFdroidDebugAndroidTest" }
|
||||
.configureEach { mustRunAfter("connectedGoogleDebugAndroidTest") }
|
||||
|
||||
kotlin { compilerOptions { jvmTarget.set(JvmTarget.JVM_21) } }
|
||||
|
||||
dependencies {
|
||||
|
||||
+3
-2
@@ -42,6 +42,7 @@ class StoreScreenshots {
|
||||
|
||||
private val arguments = InstrumentationRegistry.getArguments()
|
||||
private val appId = requireNotNull(arguments.getString("targetAppId")) { "targetAppId argument missing" }
|
||||
private val flavor = requireNotNull(arguments.getString("flavor")) { "flavor argument missing" }
|
||||
|
||||
// Shared media storage: the one app directory the shell user can read, so [save] can copy out of it.
|
||||
@Suppress("DEPRECATION")
|
||||
@@ -172,7 +173,7 @@ class StoreScreenshots {
|
||||
val file = File(mediaDir, name)
|
||||
file.parentFile?.mkdirs()
|
||||
file.outputStream().use { bitmap.compress(Bitmap.CompressFormat.PNG, PNG_QUALITY, it) }
|
||||
val target = "$DEVICE_OUTPUT/$name"
|
||||
val target = "$DEVICE_OUTPUT/$flavor/$name"
|
||||
shell("mkdir -p ${target.substringBeforeLast('/')}")
|
||||
shell("cp ${file.path} $target")
|
||||
Log.i(TAG, "saved $target")
|
||||
@@ -208,7 +209,7 @@ class StoreScreenshots {
|
||||
private companion object {
|
||||
const val TAG = "StoreScreenshots"
|
||||
|
||||
/** Where the captures are left for `adb pull`, laid out as fastlane's `images/` folder. */
|
||||
/** Where the captures are left for `adb pull`: a folder per flavor, laid out as fastlane's `images/`. */
|
||||
const val DEVICE_OUTPUT = "/data/local/tmp/store-screenshots"
|
||||
|
||||
/** Demo Mode's hidden showcase mesh; `MockScenario.SHOWCASE` in `:core:network`. */
|
||||
|
||||
Reference in new issue
Block a user