mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-07-17 16:52:31 -04:00
402 lines
15 KiB
YAML
402 lines
15 KiB
YAML
name: Promote 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
|
|
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
|
|
type: string
|
|
from_channel:
|
|
description: 'The channel to promote from'
|
|
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
|
|
|
|
concurrency:
|
|
group: ${{ github.workflow }}-${{ inputs.tag_name }}
|
|
cancel-in-progress: true
|
|
|
|
permissions:
|
|
contents: write
|
|
pull-requests: write
|
|
statuses: write
|
|
id-token: write
|
|
attestations: write
|
|
|
|
jobs:
|
|
prepare-build-info:
|
|
runs-on: ubuntu-24.04-arm
|
|
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@v7.0.0
|
|
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
|
|
run: |
|
|
COMMIT_COUNT=$(git rev-list --count HEAD)
|
|
OFFSET=${{ steps.get_version_code_offset.outputs.VERSION_CODE_OFFSET }}
|
|
VERSION_CODE=$((COMMIT_COUNT + OFFSET))
|
|
echo "versionCode=$VERSION_CODE" >> $GITHUB_OUTPUT
|
|
shell: bash
|
|
|
|
promote-release:
|
|
runs-on: ubuntu-24.04-arm
|
|
needs: [ prepare-build-info ]
|
|
env:
|
|
FROM_TRACK: ${{ inputs.from_channel == 'closed' && 'NewAlpha' || (inputs.from_channel == 'open' && 'beta' || 'internal') }}
|
|
TO_TRACK: ${{ inputs.channel == 'closed' && 'NewAlpha' || (inputs.channel == 'open' && 'beta' || 'production') }}
|
|
USER_FRACTION: ${{ (inputs.channel == 'production' && '0.1') || (inputs.channel == 'open' && '0.5') || '1.0' }}
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v7.0.0
|
|
with:
|
|
ref: ${{ inputs.commit_sha || inputs.tag_name }}
|
|
|
|
- name: Set up Ruby
|
|
uses: ruby/setup-ruby@v1
|
|
with:
|
|
ruby-version: '4.0.4'
|
|
bundler-cache: true
|
|
|
|
- name: Decode Play Store credentials
|
|
run: echo '${{ secrets.GOOGLE_PLAY_JSON_KEY }}' > fastlane/play-store-credentials.json
|
|
|
|
- name: Promote to next channel
|
|
run: |
|
|
bundle exec fastlane supply \
|
|
--track "$FROM_TRACK" \
|
|
--track_promote_to "$TO_TRACK" \
|
|
--rollout "$USER_FRACTION" \
|
|
--changes_not_sent_for_review true \
|
|
--skip_upload_metadata \
|
|
--skip_upload_changelogs \
|
|
--skip_upload_images \
|
|
--skip_upload_screenshots
|
|
|
|
- name: Clean up credentials
|
|
if: always()
|
|
run: rm -f fastlane/play-store-credentials.json
|
|
|
|
update-github-release:
|
|
runs-on: ubuntu-24.04-arm
|
|
needs: [ prepare-build-info, 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.
|
|
permissions:
|
|
contents: write
|
|
pull-requests: write
|
|
statuses: write
|
|
actions: write
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v7.0.0
|
|
with:
|
|
ref: ${{ inputs.commit_sha || inputs.tag_name }}
|
|
fetch-depth: 0
|
|
submodules: 'recursive'
|
|
|
|
- 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 }}
|
|
|
|
- name: Update GitHub Release with gh CLI
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
gh release edit ${{ inputs.tag_name }} \
|
|
--tag ${{ inputs.final_tag }} \
|
|
--title "${{ inputs.release_name }} (${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }})" \
|
|
--draft=false \
|
|
--prerelease=${{ inputs.channel != 'production' }}
|
|
|
|
# The edit above is made with GITHUB_TOKEN, and GITHUB_TOKEN-caused
|
|
# events never start workflow runs — winget-publish.yml's `released`
|
|
# trigger will not fire from it. workflow_dispatch is exempt from that
|
|
# suppression, so dispatch it explicitly. Soft-fail: a winget hiccup
|
|
# must not block the changelog stamp or Discord notification.
|
|
- name: Dispatch winget publish
|
|
if: ${{ inputs.channel == 'production' }}
|
|
continue-on-error: true
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
TAG: ${{ inputs.final_tag }}
|
|
run: gh workflow run winget-publish.yml --ref main -f "tag=$TAG"
|
|
|
|
# Same GITHUB_TOKEN-suppression story as the winget dispatch above.
|
|
- name: Dispatch Microsoft Store publish
|
|
if: ${{ inputs.channel == 'production' }}
|
|
continue-on-error: true
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
TAG: ${{ inputs.final_tag }}
|
|
run: gh workflow run msstore-publish.yml --ref main -f "tag=$TAG"
|
|
|
|
- name: Stamp CHANGELOG.md for release
|
|
if: ${{ inputs.channel == 'production' }}
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
TAG: ${{ inputs.final_tag }}
|
|
VERSION: ${{ inputs.base_version }}
|
|
REPO: ${{ github.repository }}
|
|
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
|
|
|
|
# Find the previous production tag for the full-range notes
|
|
PREV_PROD=$(git tag --list 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | grep -v "^v${VERSION}$" | head -1)
|
|
|
|
# Generate a single flat changelog for the entire release using GitHub API
|
|
FLAT_NOTES=""
|
|
if [ -n "$PREV_PROD" ]; then
|
|
FLAT_NOTES=$(gh api "repos/${REPO}/releases/generate-notes" \
|
|
-f tag_name="$TAG" \
|
|
-f target_commitish="$(git rev-parse "$TAG")" \
|
|
-f previous_tag_name="$PREV_PROD" \
|
|
--jq '.body' 2>/dev/null || true)
|
|
|
|
# Strip boilerplate
|
|
FLAT_NOTES=$(echo "$FLAT_NOTES" \
|
|
| sed '/^<!-- .* -->/d' \
|
|
| sed '/^## What'\''s Changed$/d' \
|
|
| sed '/^\*\*Full Changelog\*\*/d' \
|
|
| sed '/./,$!d' \
|
|
| sed -e :a -e '/^\n*$/{$d;N;ba}')
|
|
fi
|
|
|
|
# Build the versioned section and reset unreleased
|
|
python3 -c "
|
|
import sys
|
|
version, date, notes = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
with open('CHANGELOG.md', 'r') as f:
|
|
content = f.read()
|
|
|
|
us = '<!-- UNRELEASED_START -->'
|
|
ue = '<!-- UNRELEASED_END -->'
|
|
rs = '<!-- RELEASED_START -->'
|
|
|
|
start = content.find(us)
|
|
end = content.find(ue)
|
|
rstart = content.find(rs)
|
|
|
|
if start == -1 or end == -1 or rstart == -1:
|
|
print('Markers not found in CHANGELOG.md')
|
|
sys.exit(1)
|
|
|
|
versioned = f'## [{version}] - {date}\n\n{notes}'
|
|
fresh = '## [Unreleased]\n\n*No changes yet.*'
|
|
|
|
new_content = (
|
|
content[:start + len(us)] + '\n' + fresh + '\n'
|
|
+ content[end:rstart + len(rs)] + '\n\n' + versioned + '\n'
|
|
+ content[rstart + len(rs):]
|
|
)
|
|
|
|
with open('CHANGELOG.md', 'w') as f:
|
|
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
|
|
|
|
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"
|
|
|
|
# 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: Notify Discord
|
|
if: ${{ inputs.channel != 'internal' }}
|
|
env:
|
|
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_ANDROID }}
|
|
VERSION: ${{ inputs.final_tag }}
|
|
CHANNEL: ${{ inputs.channel }}
|
|
run: |
|
|
if [[ -z "$DISCORD_WEBHOOK" ]]; then
|
|
echo "No Discord webhook provided. Skipping notification."
|
|
exit 0
|
|
fi
|
|
|
|
# Determine Track Name for Display
|
|
if [ "$CHANNEL" == "closed" ]; then TRACK="Alpha (Closed)"; fi
|
|
if [ "$CHANNEL" == "open" ]; then TRACK="Beta (Open)"; fi
|
|
if [ "$CHANNEL" == "production" ]; then TRACK="Production"; fi
|
|
|
|
# Construct JSON Payload
|
|
PAYLOAD=$(cat <<EOF
|
|
{
|
|
"content": null,
|
|
"embeds": [
|
|
{
|
|
"title": "🚀 New Android Release: $VERSION",
|
|
"description": "A new build has been promoted to the **$TRACK** track.",
|
|
"color": 5763719,
|
|
"fields": [
|
|
{
|
|
"name": "Track",
|
|
"value": "$TRACK",
|
|
"inline": true
|
|
},
|
|
{
|
|
"name": "Version",
|
|
"value": "$VERSION",
|
|
"inline": true
|
|
}
|
|
],
|
|
"url": "https://github.com/meshtastic/Meshtastic-Android/releases/tag/$VERSION"
|
|
}
|
|
]
|
|
}
|
|
EOF
|
|
)
|
|
|
|
curl -H "Content-Type: application/json" -d "$PAYLOAD" "$DISCORD_WEBHOOK"
|
|
|
|
# Runs in-workflow rather than on `release: published` because the undraft above
|
|
# uses github.token, whose events never trigger other workflows. Once the cask is
|
|
# promoted to homebrew/cask, replace the tap PR with `brew bump-cask-pr`.
|
|
update-homebrew-cask:
|
|
if: ${{ inputs.channel == 'production' }}
|
|
runs-on: ubuntu-24.04-arm
|
|
needs: [ update-github-release ]
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v7.0.0
|
|
with:
|
|
ref: ${{ inputs.commit_sha || inputs.tag_name }}
|
|
|
|
- name: Render cask and open PR against meshtastic/homebrew-tap
|
|
env:
|
|
TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
|
|
TAG: ${{ inputs.final_tag }}
|
|
run: |
|
|
if [[ -z "$TAP_TOKEN" ]]; then
|
|
echo "HOMEBREW_TAP_TOKEN not set; skipping Homebrew cask update."
|
|
exit 0
|
|
fi
|
|
# Tags cut before the template landed check out without it — skip, don't fail.
|
|
if [[ ! -f .github/homebrew/meshtastic-desktop.rb ]]; then
|
|
echo "Cask template not present at this tag; skipping Homebrew cask update."
|
|
exit 0
|
|
fi
|
|
|
|
VERSION=${TAG#v}
|
|
# GitHub rewrites spaces in asset names to dots:
|
|
# "Meshtastic Desktop-X.dmg" is served as "Meshtastic.Desktop-X.dmg".
|
|
# --retry-all-errors covers the brief 404 window while the undraft propagates.
|
|
curl -fsSL --retry 5 --retry-delay 30 --retry-all-errors -o /tmp/app.dmg \
|
|
"https://github.com/${{ github.repository }}/releases/download/${TAG}/Meshtastic.Desktop-${VERSION}.dmg"
|
|
SHA256=$(sha256sum /tmp/app.dmg | cut -d' ' -f1)
|
|
|
|
sed -e "s/{{VERSION}}/${VERSION}/" -e "s/{{SHA256}}/${SHA256}/" \
|
|
.github/homebrew/meshtastic-desktop.rb > /tmp/meshtastic-desktop.rb
|
|
|
|
# PR (not a direct push to main) so the tap's brew test-bot CI validates the bump.
|
|
git clone "https://x-access-token:${TAP_TOKEN}@github.com/meshtastic/homebrew-tap.git" /tmp/tap
|
|
cd /tmp/tap
|
|
BRANCH="meshtastic-desktop-${VERSION}"
|
|
git checkout -b "$BRANCH"
|
|
mkdir -p Casks
|
|
cp /tmp/meshtastic-desktop.rb Casks/meshtastic-desktop.rb
|
|
git config user.name "github-actions[bot]"
|
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
git add Casks/meshtastic-desktop.rb
|
|
if git diff --cached --quiet; then
|
|
echo "Cask already up to date."
|
|
exit 0
|
|
fi
|
|
git commit -m "meshtastic-desktop ${VERSION}"
|
|
git push -fu origin "$BRANCH"
|
|
GH_TOKEN="$TAP_TOKEN" gh pr create --repo meshtastic/homebrew-tap \
|
|
--head "$BRANCH" --base main \
|
|
--title "meshtastic-desktop ${VERSION}" \
|
|
--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."
|