Skip to content

Rewrite background-location disclosure, make settings honest #77

Rewrite background-location disclosure, make settings honest

Rewrite background-location disclosure, make settings honest #77

Workflow file for this run

name: Release
on:
push:
tags:
- "[0-9]+.[0-9]+.[0-9]+"
jobs:
# ============================================
# Find and wait for watchOS build
# ============================================
find-and-wait:
name: Find watchOS Build
runs-on: ubuntu-latest
outputs:
run_id: ${{ steps.find.outputs.run_id }}
steps:
- name: Find or wait for main build
id: find
env:
GH_TOKEN: ${{ github.token }}
run: |
COMMIT_SHA="${{ github.sha }}"
MAX_WAIT=3600
POLL_INTERVAL=30
WAITED=0
while [ $WAITED -lt $MAX_WAIT ]; do
RESULT=$(gh api repos/${{ github.repository }}/actions/workflows/build.yml/runs \
--jq ".workflow_runs[] | select(.head_sha==\"$COMMIT_SHA\") | {id, status, conclusion}" \
| head -1)
if [ -z "$RESULT" ]; then
echo "::error::No build found for commit $COMMIT_SHA"
exit 1
fi
STATUS=$(echo "$RESULT" | jq -r '.status')
CONCLUSION=$(echo "$RESULT" | jq -r '.conclusion')
RUN_ID=$(echo "$RESULT" | jq -r '.id')
if [ "$STATUS" = "completed" ]; then
if [ "$CONCLUSION" = "success" ]; then
echo "run_id=$RUN_ID" >> $GITHUB_OUTPUT
echo "Found successful build: $RUN_ID"
exit 0
else
echo "::error::Build $RUN_ID failed: $CONCLUSION"
exit 1
fi
fi
echo "Build $RUN_ID still running ($STATUS). Waiting ${POLL_INTERVAL}s..."
sleep $POLL_INTERVAL
WAITED=$((WAITED + POLL_INTERVAL))
done
echo "::error::Timeout waiting for build"
exit 1
# ============================================
# Build Garmin .iq package
# ============================================
build-garmin:
name: Build Garmin App
runs-on: ubuntu-latest
container:
image: ghcr.io/matco/connectiq-tester:latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Check for Garmin credentials
id: check_creds
env:
GARMIN_DEVELOPER_KEY: ${{ secrets.GARMIN_DEVELOPER_KEY }}
run: |
if [ -z "$GARMIN_DEVELOPER_KEY" ]; then
echo "has_creds=false" >> $GITHUB_OUTPUT
echo "::notice::GARMIN_DEVELOPER_KEY not configured - skipping Garmin build"
else
echo "has_creds=true" >> $GITHUB_OUTPUT
fi
- name: Install developer key
if: steps.check_creds.outputs.has_creds == 'true'
env:
GARMIN_DEVELOPER_KEY: ${{ secrets.GARMIN_DEVELOPER_KEY }}
run: |
mkdir -p ~/.Garmin
echo "$GARMIN_DEVELOPER_KEY" | base64 -d > ~/.Garmin/developer_key.der
- name: Generate Garmin secrets
if: steps.check_creds.outputs.has_creds == 'true'
env:
TRAINTIME_API_KEY: ${{ secrets.TRAINTIME_API_KEY }}
run: |
cat > garmin/TrainTime/source/Secrets.mc <<EOF
module Secrets {
const API_KEY = "$TRAINTIME_API_KEY";
}
EOF
- name: Build .iq package
if: steps.check_creds.outputs.has_creds == 'true'
working-directory: garmin/TrainTime
run: |
monkeyc -e -f monkey.jungle -o ../TrainTime.iq -y ~/.Garmin/developer_key.der -r
echo "Built TrainTime.iq ($(stat -c%s ../TrainTime.iq) bytes)"
- name: Upload Garmin artifact
if: steps.check_creds.outputs.has_creds == 'true'
uses: actions/upload-artifact@v4
with:
name: garmin-iq
path: garmin/TrainTime.iq
retention-days: 30
# ============================================
# Build Android .aab package
# ============================================
build-android:
name: Build Android App
runs-on: ubuntu-latest
steps:
- name: Check for Android signing credentials
id: check_creds
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
run: |
if [ -z "$ANDROID_KEYSTORE_BASE64" ]; then
echo "has_creds=false" >> $GITHUB_OUTPUT
echo "::notice::ANDROID_KEYSTORE_BASE64 not configured - skipping Android build"
else
echo "has_creds=true" >> $GITHUB_OUTPUT
fi
- name: Checkout
if: steps.check_creds.outputs.has_creds == 'true'
uses: actions/checkout@v4
- name: Verify tag matches versionName
if: steps.check_creds.outputs.has_creds == 'true'
run: |
VN=$(grep 'versionName = ' android/app/build.gradle.kts | sed 's/.*"\(.*\)".*/\1/')
VN_WEAR=$(grep 'versionName = ' android/wear/build.gradle.kts | sed 's/.*"\(.*\)".*/\1/')
echo "Tag: ${{ github.ref_name }} | app versionName: $VN | wear versionName: $VN_WEAR"
if [ "$VN" != "${{ github.ref_name }}" ]; then
echo "::error::Tag ${{ github.ref_name }} does not match versionName $VN. Bump versionName (and versionCode) in android/app/build.gradle.kts before tagging."
exit 1
fi
if [ "$VN_WEAR" != "${{ github.ref_name }}" ]; then
echo "::error::Tag ${{ github.ref_name }} does not match wear versionName $VN_WEAR. Bump versionName (and versionCode) in android/wear/build.gradle.kts before tagging."
exit 1
fi
- name: Setup Java
if: steps.check_creds.outputs.has_creds == 'true'
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "17"
- name: Setup Gradle
if: steps.check_creds.outputs.has_creds == 'true'
uses: gradle/actions/setup-gradle@v4
- name: Decode signing keystore
if: steps.check_creds.outputs.has_creds == 'true'
working-directory: android
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
run: echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > upload-keystore.jks
- name: Build release AAB
if: steps.check_creds.outputs.has_creds == 'true'
working-directory: android
env:
TRAINTIME_API_KEY: ${{ secrets.TRAINTIME_API_KEY }}
KEYSTORE_FILE: upload-keystore.jks
KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
run: |
./gradlew :app:bundleRelease :wear:bundleRelease
mkdir -p dist
cp app/build/outputs/bundle/release/app-release.aab dist/
cp wear/build/outputs/bundle/release/wear-release.aab dist/
echo "Built app-release.aab ($(stat -c%s dist/app-release.aab) bytes), wear-release.aab ($(stat -c%s dist/wear-release.aab) bytes)"
- name: Upload Android artifact
if: steps.check_creds.outputs.has_creds == 'true'
uses: actions/upload-artifact@v4
with:
name: android-aab
path: android/dist/*.aab
retention-days: 30
# ============================================
# Create Release
# ============================================
release:
name: Create Release
needs: [find-and-wait, build-garmin, build-android]
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
version: ${{ steps.version.outputs.version }}
ipa_path: ${{ steps.prepare.outputs.ipa_path }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Get version from tag
id: version
run: echo "version=${{ github.ref_name }}" >> $GITHUB_OUTPUT
- name: Download iOS/watchOS artifacts from main build
uses: actions/download-artifact@v4
with:
run-id: ${{ needs.find-and-wait.outputs.run_id }}
github-token: ${{ github.token }}
path: artifacts
- name: Download Garmin artifact
uses: actions/download-artifact@v4
continue-on-error: true
with:
name: garmin-iq
path: artifacts/garmin
- name: Download Android artifact
uses: actions/download-artifact@v4
continue-on-error: true
with:
name: android-aab
path: artifacts/android
- name: Prepare release artifacts
id: prepare
run: |
mkdir -p release
VERSION="${{ steps.version.outputs.version }}"
echo "=== Downloaded artifacts ==="
find artifacts -type f
echo "============================="
IPA=$(find artifacts -name "*.ipa" | head -1)
if [ -n "$IPA" ]; then
IPA_DEST="release/traintime-${VERSION}.ipa"
cp "$IPA" "$IPA_DEST"
echo "ipa_path=$IPA_DEST" >> $GITHUB_OUTPUT
echo "IPA: traintime-${VERSION}.ipa"
else
echo "::warning::No IPA found in artifacts"
fi
IQ=$(find artifacts -name "*.iq" | head -1)
if [ -n "$IQ" ]; then
cp "$IQ" "release/TrainTime-${VERSION}.iq"
echo "Garmin: TrainTime-${VERSION}.iq"
else
echo "::notice::No Garmin .iq found"
fi
AAB=$(find artifacts -name "app-release.aab" | head -1)
if [ -n "$AAB" ]; then
cp "$AAB" "release/traintime-${VERSION}.aab"
echo "Android: traintime-${VERSION}.aab"
else
echo "::notice::No Android .aab found"
fi
WEAR_AAB=$(find artifacts -name "wear-release.aab" | head -1)
if [ -n "$WEAR_AAB" ]; then
cp "$WEAR_AAB" "release/traintime-wear-${VERSION}.aab"
echo "Wear OS: traintime-wear-${VERSION}.aab"
else
echo "::notice::No Wear OS .aab found"
fi
echo ""
echo "=== Release artifacts ==="
ls -la release/
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
name: TrainTime v${{ steps.version.outputs.version }}
body: |
## TrainTime v${{ steps.version.outputs.version }}
## Downloads
- **iOS IPA**: For TestFlight / App Store (includes iOS app, watchOS companion, widget)
- **Garmin .iq**: Upload to [Connect IQ Store](https://apps.garmin.com/developer/dashboard)
- **Android .aab**: Signed release bundle for the [Play Console](https://play.google.com/console)
- **Wear OS .aab**: Signed watch bundle, uploaded to the same Play release
---
Built from commit: ${{ github.sha }}
files: release/*
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload IPA for deployment
if: steps.prepare.outputs.ipa_path != ''
uses: actions/upload-artifact@v4
with:
name: release-ipa
path: ${{ steps.prepare.outputs.ipa_path }}
retention-days: 1
# ============================================
# Deploy to TestFlight
# ============================================
deploy-testflight:
name: Deploy to TestFlight
needs: release
runs-on: macos-26
if: needs.release.outputs.ipa_path != ''
steps:
- name: Check for TestFlight credentials
id: check_creds
env:
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
ASC_API_KEY: ${{ secrets.ASC_API_KEY }}
run: |
if [ -z "$ASC_KEY_ID" ] || [ -z "$ASC_ISSUER_ID" ] || [ -z "$ASC_API_KEY" ]; then
echo "skip=true" >> $GITHUB_OUTPUT
echo "::notice::Skipping TestFlight - credentials not configured"
else
echo "skip=false" >> $GITHUB_OUTPUT
fi
- name: Checkout repository
if: steps.check_creds.outputs.skip != 'true'
uses: actions/checkout@v4
- name: Setup Ruby
if: steps.check_creds.outputs.skip != 'true'
uses: ruby/setup-ruby@v1
with:
ruby-version: "3.2"
working-directory: apple/fastlane
bundler-cache: true
- name: Download IPA
if: steps.check_creds.outputs.skip != 'true'
uses: actions/download-artifact@v4
with:
name: release-ipa
path: release/
- name: Upload to TestFlight
if: steps.check_creds.outputs.skip != 'true'
working-directory: apple/fastlane
env:
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
ASC_API_KEY: ${{ secrets.ASC_API_KEY }}
IPA_PATH: ${{ github.workspace }}/release/traintime-${{ needs.release.outputs.version }}.ipa
run: |
if [ ! -f "$IPA_PATH" ]; then
echo "Error: IPA file not found at $IPA_PATH"
ls -la "${{ github.workspace }}/release/" || echo "Directory not found"
exit 1
fi
bundle exec fastlane ios beta ipa:"$IPA_PATH"
- name: Summary
if: steps.check_creds.outputs.skip != 'true'
run: |
echo "## TestFlight Deployment" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Version:** ${{ needs.release.outputs.version }}" >> $GITHUB_STEP_SUMMARY
echo "- **Track:** Internal TestFlight" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Build will be available in [App Store Connect](https://appstoreconnect.apple.com) shortly." >> $GITHUB_STEP_SUMMARY
# ============================================
# Prepare App Store version + upload metadata
# ============================================
prepare-appstore:
name: Prepare App Store version
needs: release
runs-on: ubuntu-latest
steps:
- name: Check for App Store Connect credentials
id: check_creds
env:
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
ASC_API_KEY: ${{ secrets.ASC_API_KEY }}
run: |
if [ -z "$ASC_KEY_ID" ] || [ -z "$ASC_ISSUER_ID" ] || [ -z "$ASC_API_KEY" ]; then
echo "skip=true" >> $GITHUB_OUTPUT
echo "::notice::Skipping App Store metadata - credentials not configured"
else
echo "skip=false" >> $GITHUB_OUTPUT
fi
- name: Checkout repository
if: steps.check_creds.outputs.skip != 'true'
uses: actions/checkout@v4
- name: Verify tag matches MARKETING_VERSION
if: steps.check_creds.outputs.skip != 'true'
run: |
MV=$(grep 'MARKETING_VERSION' apple/TrainTimeWatch.xcodeproj/project.pbxproj | head -1 | sed 's/.*= *\(.*\);/\1/' | tr -d ' ')
echo "Tag: ${{ github.ref_name }} | MARKETING_VERSION: $MV"
if [ "$MV" != "${{ github.ref_name }}" ]; then
echo "::error::Tag ${{ github.ref_name }} does not match MARKETING_VERSION $MV. Bump the version in Xcode before tagging."
exit 1
fi
- name: Setup Ruby
if: steps.check_creds.outputs.skip != 'true'
uses: ruby/setup-ruby@v1
with:
ruby-version: "3.2"
working-directory: apple/fastlane
bundler-cache: true
- name: Create App Store version and upload metadata
if: steps.check_creds.outputs.skip != 'true'
working-directory: apple/fastlane
env:
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
ASC_API_KEY: ${{ secrets.ASC_API_KEY }}
run: bundle exec fastlane ios sync_metadata version:${{ github.ref_name }}
- name: Summary
if: steps.check_creds.outputs.skip != 'true'
run: |
echo "## App Store Version Prepared" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Version:** ${{ github.ref_name }} (Prepare for Submission, not submitted)" >> $GITHUB_STEP_SUMMARY
echo "- **Metadata:** uploaded from apple/fastlane/metadata/" >> $GITHUB_STEP_SUMMARY
# ============================================
# Deploy to Google Play
# ============================================
deploy-android:
name: Deploy to Google Play
needs: [release, build-android]
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- name: Check for Play credentials
id: check_creds
env:
PLAY_SERVICE_ACCOUNT_JSON: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }}
run: |
if [ -z "$PLAY_SERVICE_ACCOUNT_JSON" ]; then
echo "skip=true" >> $GITHUB_OUTPUT
echo "::notice::Skipping Google Play - PLAY_SERVICE_ACCOUNT_JSON not configured"
else
echo "skip=false" >> $GITHUB_OUTPUT
fi
- name: Checkout repository
if: steps.check_creds.outputs.skip != 'true'
uses: actions/checkout@v4
- name: Setup Ruby
if: steps.check_creds.outputs.skip != 'true'
uses: ruby/setup-ruby@v1
with:
ruby-version: "3.2"
working-directory: android
bundler-cache: true
- name: Download Android AAB
if: steps.check_creds.outputs.skip != 'true'
uses: actions/download-artifact@v4
with:
name: android-aab
path: android/release
- name: Write Play credentials
if: steps.check_creds.outputs.skip != 'true'
working-directory: android
env:
PLAY_SERVICE_ACCOUNT_JSON: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }}
run: echo "$PLAY_SERVICE_ACCOUNT_JSON" > play-service-account.json
- name: Upload to Google Play
if: steps.check_creds.outputs.skip != 'true'
working-directory: android
env:
SUPPLY_JSON_KEY: ${{ github.workspace }}/android/play-service-account.json
run: bundle exec fastlane android ci_release aab:"${{ github.workspace }}/android/release/app-release.aab" wear_aab:"${{ github.workspace }}/android/release/wear-release.aab"
- name: Summary
if: steps.check_creds.outputs.skip != 'true'
run: |
echo "## Google Play Deployment" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Version:** ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY
echo "- **Internal testing:** published (live to internal testers, phone + Wear OS)" >> $GITHUB_STEP_SUMMARY
echo "- **Open testing:** staged as a draft (review and roll out in the console)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Build will appear in the [Play Console](https://play.google.com/console) shortly." >> $GITHUB_STEP_SUMMARY