Skip to content

Tag and build release #198

Tag and build release

Tag and build release #198

Workflow file for this run

name: Tag and build release
env:
# The name of the main module repository
main_project_module: app
# The name in the Play Store
playstore_name: Soundscape
on:
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
jobs:
prepare-version:
runs-on: ubuntu-latest
environment: development
permissions:
contents: write
outputs:
version_code: ${{ steps.current-version.outputs.VERSION_CODE }}
version_name: ${{ steps.current-version.outputs.VERSION_NAME }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
token: ${{ secrets.PAT_TOKEN }}
- name: Extract existing version code from build.gradle
id: current-version
run: |
# Get existing version code from build.gradle
version_code=$(grep "versionCode" app/build.gradle.kts | awk '{print $3}' | tr -d '\n')
version_name=$(grep "versionName =" app/build.gradle.kts | awk '{print $3}' | tr -d '\"\"')
major_version=$(echo $version_name | awk -F \. {'print $1'})
minor_version=$(echo $version_name | awk -F \. {'print $2'})
build_version=$(echo $version_name | awk -F \. {'print $3'})
# Increment existing version code and build version by 1
version_code=$((version_code + 1))
build_version=$((build_version + 1))
# The major and minor versions can be bumped manually by editing
# app/build.gradle.kts. When doing this, reset the build version
# to zero.
# Set output variables for later use
echo "VERSION_CODE=$version_code" >> $GITHUB_OUTPUT
echo "VERSION_NAME=$major_version.$minor_version.$build_version" >> $GITHUB_OUTPUT
- name: Increase version code and change version name
env:
VERSION_CODE: ${{ steps.current-version.outputs.VERSION_CODE }}
VERSION_NAME: ${{ steps.current-version.outputs.VERSION_NAME }}
run: |
# Update build.gradle with new version code and name
echo "$VERSION_CODE - $env.VERSION_NAME"
sed -i "s/versionCode = [0-9]\+/versionCode = $VERSION_CODE/g" app/build.gradle.kts
sed -i "s/versionName = \"[^\"]*\"/versionName = \"$VERSION_NAME\"/g" app/build.gradle.kts
- name: Bump version from "${{ steps.current-version.outputs.VERSION_NAME }}"
env:
VERSION_CODE: ${{ steps.current-version.outputs.VERSION_CODE }}
VERSION_NAME: ${{ steps.current-version.outputs.VERSION_NAME }}
uses: stefanzweifel/git-auto-commit-action@v7
with:
commit_message: "Bump version to ${{ env.VERSION_NAME }}, version code ${{ env.VERSION_CODE }}"
tagging_message: "soundscape-${{ env.VERSION_NAME }}"
build:
needs: prepare-version
runs-on: ubuntu-latest
environment: development
permissions:
contents: write
steps:
# Check out the branch tip so we pick up the version-bump commit
# that prepare-version pushed.
- uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ github.ref }}
token: ${{ secrets.PAT_TOKEN }}
# actions/checkout shortcuts to github.sha (the trigger SHA) when the
# given ref matches github.ref, so we'd otherwise miss the version-bump
# commit pushed by prepare-version. Fast-forward to the real remote tip
# so the later screenshot auto-commit can push without conflict.
- name: Fast-forward to remote tip
run: git pull --ff-only origin ${{ github.ref_name }}
- name: Setup offline maps for tests
uses: ./.github/actions/setup-offline-maps
with:
output_dir: ${{ env.main_project_module }}/src/test/res/org/scottishtecharmy/soundscape
# Set version
- name: Set 'git describe --tags' as version
id: get-version
run: echo "VERSION=$(git describe --tags)" >> $GITHUB_OUTPUT
- name: Setup tile and search providers
uses: ./.github/actions/write-provider-config
with:
platform: android
tile_provider_url: ${{ secrets.TILE_PROVIDER_URL }}
tile_provider_api_key: ${{ secrets.TILE_PROVIDER_API_KEY }}
search_provider_url: ${{ secrets.SEARCH_PROVIDER_URL }}
search_provider_api_key: ${{ secrets.SEARCH_PROVIDER_API_KEY }}
extract_provider_url: ${{ secrets.EXTRACT_PROVIDER_URL }}
- name: Decode Google services
uses: ./.github/actions/decode-base64-secret
with:
encoded: ${{ secrets.GOOGLE_SERVICES }}
output_path: ${{ env.main_project_module }}/${{ secrets.GOOGLE_SERVICES_PATH }}
- name: Setup JDK + Gradle
uses: ./.github/actions/setup-jdk-gradle
- name: Run lint over the repo
run: ./gradlew lint
# Setup release build keys
- name: Decode Keystore
uses: ./.github/actions/decode-base64-secret
with:
encoded: ${{ secrets.SIGNING_KEY_STORE_BASE64 }}
output_path: ${{ env.main_project_module }}/${{ secrets.SIGNING_KEY_STORE_PATH }}
# Run Tests Build
- name: Run gradle tests
run: ./gradlew test
#
# This is fragile with small changes causing it to run out of memory. Comment out until Google fixes it
#
# - name: Generate test screenshots
# run: |
# ./gradlew updateDebugScreenshotTest
# ./categorise.sh app/src/screenshotTestDebug/reference/org/scottishtecharmy/soundscape/ThemeTestClass/
# cd app/src/screenshotTestDebug/reference/org/scottishtecharmy/soundscape/ThemeTestClass/
# zip -r screenshots.zip .
#
# - name: Upload screenshots
# uses: actions/upload-artifact@v6
# with:
# name: test-screenshots
# path: app/src/screenshotTestDebug/reference/org/scottishtecharmy/soundscape/ThemeTestClass/screenshots.zip
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Build instrumentation tests to generate screenshots for documentation
id: documentation-screenshot-tests
uses: reactivecircus/android-emulator-runner@v2
continue-on-error: true
with:
api-level: 34
target: default
arch: x86_64
profile: pixel_7
script: |
mkdir -p docs/documentationScreens
adb logcat -c # clear logs
adb shell settings put global policy_control immersive.status=* # Disable status bar at top of screen
touch app/emulator.log # create log file
chmod 777 app/emulator.log # allow writing to log file
adb logcat >> app/emulator.log & # pipe all logcat messages into log file as a background process
# Run only the DocumentationScreens tests
./gradlew connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=org.scottishtecharmy.soundscape.DocumentationScreens
# And then get the resulting screenshots from the emulator
adb pull /storage/emulated/0/Android/data/org.scottishtecharmy.soundscape/files/Pictures/screenshots/homeScreen.png docs/documentationScreens/homeScreen.png
adb pull /storage/emulated/0/Android/data/org.scottishtecharmy.soundscape/files/Pictures/screenshots/homeScreenWithRoute.png docs/documentationScreens/homeScreenWithRoute.png
adb pull /storage/emulated/0/Android/data/org.scottishtecharmy.soundscape/files/Pictures/screenshots/routeDetails.png docs/documentationScreens/routeDetails.png
adb pull /storage/emulated/0/Android/data/org.scottishtecharmy.soundscape/files/Pictures/screenshots/routeEdit.png docs/documentationScreens/routeEdit.png
adb pull /storage/emulated/0/Android/data/org.scottishtecharmy.soundscape/files/Documents/help docs/users/
mv docs/users/help/* docs/users/
- name: ImageMagick Action to trim off status and navigation bars from screenshots
uses: jruipinto/ImageMagick-action@v1
with:
command: mogrify -path docs/documentationScreens -crop 1080x2130+0+140 docs/documentationScreens/*.*
- name: Commit new screenshots
uses: stefanzweifel/git-auto-commit-action@v7
with:
commit_message: "New documentation screenshots"
file_pattern: 'docs/documentationScreens/*.png docs/users/help-*.md'
# Run Build Project
- name: Build gradle project
env:
SIGNING_KEY_STORE_PATH: ${{ secrets.SIGNING_KEY_STORE_PATH }}
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
run: |
./gradlew build
# Create APK Release
- name: Build apk release project (APK)
env:
SIGNING_KEY_STORE_PATH: ${{ secrets.SIGNING_KEY_STORE_PATH }}
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
run: ./gradlew assemble
# Create Bundle AAB Release
# Noted for main module build [main_project_module]:bundleRelease
- name: Build app bundle release (AAB)
env:
SIGNING_KEY_STORE_PATH: ${{ secrets.SIGNING_KEY_STORE_PATH }}
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
run: ./gradlew ${{ env.main_project_module }}:bundleRelease
# Upload Artifact Build
# Noted For Output [main_project_module]/build/outputs/apk/releaseTest/
- name: Upload APK ReleaseTest
uses: actions/upload-artifact@v6
with:
name: releaseTest-apk
path: |
${{ env.main_project_module }}/build/outputs/apk/releaseTest/
# Noted For Output [main_project_module]/build/outputs/apk/release/
- name: Upload APK Release
uses: actions/upload-artifact@v6
with:
name: release-apk
path: |
${{ env.main_project_module }}/build/outputs/apk/release/
# Noted For Output [main_project_module]/build/outputs/bundle/release/
- name: Upload AAB (App Bundle) Release
uses: actions/upload-artifact@v6
with:
name: release-aab
path: |
${{ env.main_project_module }}/build/outputs/bundle/release/
${{ env.main_project_module }}/build/outputs/mapping/release/
firebase:
name: Run UI tests with Firebase Test Lab
needs: build
environment: development
runs-on: ubuntu-latest
steps:
- name: Download app releaseTest APK
uses: actions/download-artifact@v8
with:
name: releaseTest-apk
- name: Auth
uses: google-github-actions/auth@v3
with:
credentials_json: ${{ secrets.GCLOUD_CREDENTIALS_JSON }}
- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v3
- name: Set current project
run: gcloud config set project ${{ secrets.FIREBASE_PROJECT_ID }}
- name: Run Robo test in Firebase Test Lab
run: gcloud firebase test android run --type robo --app=app-releaseTest.apk --timeout=300s --device model=shiba,version=34,locale=en,orientation=portrait
ios-build:
name: Build and sign iOS app
needs: prepare-version
runs-on: [self-hosted, macOS, ARM64]
environment: development
permissions:
contents: write
steps:
# Pull the latest commit on the branch (including the version-bump
# commit produced by the Android `build` job).
- uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ github.ref }}
token: ${{ secrets.PAT_TOKEN }}
- name: Setup iOS toolchain
uses: ./.github/actions/setup-ios-toolchain
- name: Setup tile and search providers
uses: ./.github/actions/write-provider-config
with:
platform: ios
tile_provider_url: ${{ secrets.TILE_PROVIDER_URL }}
search_provider_url: ${{ secrets.SEARCH_PROVIDER_URL }}
extract_provider_url: ${{ secrets.EXTRACT_PROVIDER_URL }}
- name: Generate Xcode project
working-directory: iosApp
run: xcodegen generate
# See run-tests.yaml: linking iosSimulatorArm64Test fails because of
# maplibre-compose's embedded linker flags. xcodebuild archive below
# builds via the Xcode/SPM toolchain that resolves MapLibre.
- name: Compile shared Kotlin/Native tests for iOS simulator
run: ./gradlew :shared:compileTestKotlinIosSimulatorArm64
- name: Setup iOS signing
uses: ./.github/actions/setup-ios-signing
with:
cert_base64: ${{ secrets.IOS_DIST_CERT_BASE64 }}
cert_password: ${{ secrets.IOS_DIST_CERT_PASSWORD }}
provisioning_profile_base64: ${{ secrets.IOS_PROVISIONING_PROFILE_BASE64 }}
shareext_provisioning_profile_base64: ${{ secrets.IOS_SHAREEXT_PROVISIONING_PROFILE_BASE64 }}
keychain_password: ${{ secrets.IOS_KEYCHAIN_PASSWORD }}
# CI runners have no Apple ID logged in, so xcodebuild authenticates
# via the App Store Connect API key for any -allowProvisioningUpdates
# operation; otherwise it fails with "No Accounts: Add a new account
# in Accounts settings."
- name: Decode App Store Connect API key
uses: ./.github/actions/decode-base64-secret
with:
encoded: ${{ secrets.APPSTORE_CONNECT_API_KEY_BASE64 }}
output_path: ${{ runner.temp }}/private_keys/AuthKey_${{ secrets.APPSTORE_CONNECT_API_KEY_ID }}.p8
# Reuse the version produced by the Android job. MARKETING_VERSION maps
# to CFBundleShortVersionString and CURRENT_PROJECT_VERSION to
# CFBundleVersion when passed as xcodebuild build settings.
- name: Archive iOS app
env:
VERSION_CODE: ${{ needs.prepare-version.outputs.version_code }}
VERSION_NAME: ${{ needs.prepare-version.outputs.version_name }}
KEY_ID: ${{ secrets.APPSTORE_CONNECT_API_KEY_ID }}
ISSUER_ID: ${{ secrets.APPSTORE_CONNECT_API_ISSUER_ID }}
run: |
# Strip the patch component so TestFlight groups all 2.0.x builds
# under one "2.0" train; uniqueness within the train comes from
# CURRENT_PROJECT_VERSION (the build number).
SHORT_VERSION=$(echo "$VERSION_NAME" | cut -d. -f1,2)
xcodebuild archive \
-project iosApp/iosApp.xcodeproj \
-scheme iosApp \
-configuration Release \
-sdk iphoneos \
-destination 'generic/platform=iOS' \
-archivePath "$RUNNER_TEMP/Soundscape.xcarchive" \
-authenticationKeyPath "$RUNNER_TEMP/private_keys/AuthKey_$KEY_ID.p8" \
-authenticationKeyID "$KEY_ID" \
-authenticationKeyIssuerID "$ISSUER_ID" \
-allowProvisioningUpdates \
MARKETING_VERSION="$SHORT_VERSION" \
CURRENT_PROJECT_VERSION="$VERSION_CODE" \
APP_VERSION_NAME="$VERSION_NAME"
# Manual signing during export so xcodebuild reuses the profile we
# imported in setup-ios-signing instead of asking Apple to mint a fresh
# managed profile (which may lack capabilities like Associated Domains
# that exist on the App ID but can't be granted under the API key role).
- name: Write export options
run: |
cat > "$RUNNER_TEMP/ExportOptions.plist" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key><string>app-store</string>
<key>teamID</key><string>48N3J4V5KM</string>
<key>signingStyle</key><string>manual</string>
<key>provisioningProfiles</key>
<dict>
<key>org.scottishtecharmy.soundscape</key>
<string>${IOS_PROFILE_NAME}</string>
<key>org.scottishtecharmy.soundscape.share</key>
<string>${IOS_SHAREEXT_PROFILE_NAME}</string>
</dict>
<key>uploadBitcode</key><false/>
<key>uploadSymbols</key><true/>
</dict>
</plist>
PLIST
- name: Export .ipa
env:
KEY_ID: ${{ secrets.APPSTORE_CONNECT_API_KEY_ID }}
ISSUER_ID: ${{ secrets.APPSTORE_CONNECT_API_ISSUER_ID }}
run: |
xcodebuild -exportArchive \
-archivePath "$RUNNER_TEMP/Soundscape.xcarchive" \
-exportPath "$RUNNER_TEMP/export" \
-exportOptionsPlist "$RUNNER_TEMP/ExportOptions.plist" \
-authenticationKeyPath "$RUNNER_TEMP/private_keys/AuthKey_$KEY_ID.p8" \
-authenticationKeyID "$KEY_ID" \
-authenticationKeyIssuerID "$ISSUER_ID" \
-allowProvisioningUpdates
- name: Upload .ipa
uses: actions/upload-artifact@v6
with:
name: release-ipa
path: ${{ runner.temp }}/export/*.ipa
- name: Upload .xcarchive (for dSYM/debug symbols)
uses: actions/upload-artifact@v6
with:
name: release-xcarchive
path: ${{ runner.temp }}/Soundscape.xcarchive
ios-testflight:
name: Upload iOS build to TestFlight
needs: ios-build
runs-on: macos-15
environment: development
steps:
# Local composite actions live in .github/actions/, so the workflow
# must check the repo out before referencing them.
- uses: actions/checkout@v6
- name: Download .ipa
uses: actions/download-artifact@v8
with:
name: release-ipa
path: ipa
- name: Decode App Store Connect API key
uses: ./.github/actions/decode-base64-secret
with:
encoded: ${{ secrets.APPSTORE_CONNECT_API_KEY_BASE64 }}
# altool looks for AuthKey_<KEY_ID>.p8 in ~/.appstoreconnect/private_keys
output_path: ${{ runner.temp }}/private_keys/AuthKey_${{ secrets.APPSTORE_CONNECT_API_KEY_ID }}.p8
- name: Upload to TestFlight
env:
KEY_ID: ${{ secrets.APPSTORE_CONNECT_API_KEY_ID }}
ISSUER_ID: ${{ secrets.APPSTORE_CONNECT_API_ISSUER_ID }}
API_PRIVATE_KEYS_DIR: ${{ runner.temp }}/private_keys
run: |
IPA=$(ls ipa/*.ipa | head -n1)
xcrun altool --upload-app \
--type ios \
--file "$IPA" \
--apiKey "$KEY_ID" \
--apiIssuer "$ISSUER_ID"
ios-firebase:
# iOS Firebase Test Lab supports XCTest and game-loop, but not Robo (which
# is the test type used for Android in this repo). XCTest already runs on
# the simulator in run-tests.yaml, so this job runs the same XCTest suite
# against real devices via Test Lab as a release smoke test.
name: Run iOS XCTest suite via Firebase Test Lab
needs: ios-build
runs-on: [self-hosted, macOS, ARM64]
environment: development
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup iOS toolchain
uses: ./.github/actions/setup-ios-toolchain
- name: Setup tile and search providers
uses: ./.github/actions/write-provider-config
with:
platform: ios
tile_provider_url: ${{ secrets.TILE_PROVIDER_URL }}
search_provider_url: ${{ secrets.SEARCH_PROVIDER_URL }}
extract_provider_url: ${{ secrets.EXTRACT_PROVIDER_URL }}
- name: Generate Xcode project
working-directory: iosApp
run: xcodegen generate
- name: Setup iOS signing
uses: ./.github/actions/setup-ios-signing
with:
cert_base64: ${{ secrets.IOS_DIST_CERT_BASE64 }}
cert_password: ${{ secrets.IOS_DIST_CERT_PASSWORD }}
provisioning_profile_base64: ${{ secrets.IOS_PROVISIONING_PROFILE_BASE64 }}
shareext_provisioning_profile_base64: ${{ secrets.IOS_SHAREEXT_PROVISIONING_PROFILE_BASE64 }}
keychain_password: ${{ secrets.IOS_KEYCHAIN_PASSWORD }}
- name: Decode App Store Connect API key
uses: ./.github/actions/decode-base64-secret
with:
encoded: ${{ secrets.APPSTORE_CONNECT_API_KEY_BASE64 }}
output_path: ${{ runner.temp }}/private_keys/AuthKey_${{ secrets.APPSTORE_CONNECT_API_KEY_ID }}.p8
- name: Build XCTest bundle for device
env:
KEY_ID: ${{ secrets.APPSTORE_CONNECT_API_KEY_ID }}
ISSUER_ID: ${{ secrets.APPSTORE_CONNECT_API_ISSUER_ID }}
run: |
xcodebuild build-for-testing \
-project iosApp/iosApp.xcodeproj \
-scheme iosApp \
-configuration Release \
-sdk iphoneos \
-destination 'generic/platform=iOS' \
-derivedDataPath build_for_testing \
-authenticationKeyPath "$RUNNER_TEMP/private_keys/AuthKey_$KEY_ID.p8" \
-authenticationKeyID "$KEY_ID" \
-authenticationKeyIssuerID "$ISSUER_ID" \
-allowProvisioningUpdates \
ENABLE_TESTABILITY=YES
- name: Package test bundle for Test Lab
run: |
cd build_for_testing/Build/Products
zip -r "$RUNNER_TEMP/ios_tests.zip" Release-iphoneos *.xctestrun
- name: Auth
uses: google-github-actions/auth@v3
with:
credentials_json: ${{ secrets.GCLOUD_CREDENTIALS_JSON }}
- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v3
- name: Set current project
run: gcloud config set project ${{ secrets.FIREBASE_PROJECT_ID }}
- name: Run XCTest in Firebase Test Lab
run: |
gcloud firebase test ios run \
--type xctest \
--test "$RUNNER_TEMP/ios_tests.zip" \
--timeout=1800s \
--device model=iphonese3,version=18.4,locale=en_US,orientation=portrait