diff --git a/.github/workflows/mobile-artifacts.yml b/.github/workflows/mobile-artifacts.yml
index e5bb8646..6b9e7156 100644
--- a/.github/workflows/mobile-artifacts.yml
+++ b/.github/workflows/mobile-artifacts.yml
@@ -11,6 +11,36 @@ on:
description: Store build number/version code
required: false
type: string
+ upload_android_to_play:
+ description: Upload the signed Android App Bundle to Google Play
+ required: false
+ type: boolean
+ default: true
+ upload_ios_to_app_store:
+ description: Upload the signed iOS IPA to App Store Connect
+ required: false
+ type: boolean
+ default: true
+ android_track:
+ description: Google Play track for manual dispatches
+ required: false
+ type: choice
+ default: production
+ options:
+ - production
+ - beta
+ - alpha
+ - internal
+ android_release_status:
+ description: Google Play release status for manual dispatches
+ required: false
+ type: choice
+ default: completed
+ options:
+ - completed
+ - draft
+ - inProgress
+ - halted
push:
tags:
- "mobile-v*"
@@ -26,40 +56,81 @@ permissions:
contents: read
concurrency:
- group: mobile-store-artifacts-${{ github.ref_name }}
+ group: mobile-store-artifacts-${{ github.event.workflow_run.head_branch || github.ref_name || github.run_id }}
cancel-in-progress: true
jobs:
android:
- name: Android AAB
+ name: Android AAB and Play upload
if: >
github.event_name != 'workflow_run' ||
(github.event.workflow_run.conclusion == 'success' &&
+ github.event.workflow_run.event == 'push' &&
github.event.workflow_run.head_branch == github.event.repository.default_branch)
- runs-on: ubuntu-latest
- timeout-minutes: 45
+ runs-on: ubuntu-24.04
+ environment: mobile-store-production
+ timeout-minutes: 60
+ env:
+ ANDROID_PACKAGE_NAME: ${{ vars.ANDROID_PACKAGE_NAME || 'io.truckwash.twa' }}
+ ANDROID_AAB_PATH: ${{ vars.ANDROID_AAB_PATH || 'android/app/build/outputs/bundle/release/app-release.aab' }}
+ PLAY_STORE_TRACK: ${{ inputs.android_track || vars.PLAY_STORE_TRACK || 'production' }}
+ PLAY_STORE_RELEASE_STATUS: ${{ inputs.android_release_status || vars.PLAY_STORE_RELEASE_STATUS || 'completed' }}
+ PLAY_STORE_USER_FRACTION: ${{ vars.PLAY_STORE_USER_FRACTION || '' }}
+ UPLOAD_ANDROID_TO_PLAY: ${{ github.event_name != 'workflow_dispatch' || inputs.upload_android_to_play }}
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
+ - name: Guard current master release
+ id: release-guard
+ shell: bash
+ env:
+ EVENT_NAME: ${{ github.event_name }}
+ EXPECTED_SHA: ${{ github.event.workflow_run.head_sha || github.sha }}
+ RELEASE_BRANCH: ${{ github.event.workflow_run.head_branch || github.ref_name }}
+ DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
+ run: |
+ set -euo pipefail
+ current=true
+ if [[ "$EVENT_NAME" == "workflow_run" ]]; then
+ latest_sha="$(git ls-remote origin "refs/heads/$DEFAULT_BRANCH" | awk '{print $1}')"
+ if [[ -z "$latest_sha" ]]; then
+ echo "Could not resolve origin/$DEFAULT_BRANCH." >&2
+ exit 1
+ fi
+ if [[ "$latest_sha" != "$EXPECTED_SHA" ]]; then
+ current=false
+ echo "Skipping stale mobile upload for $EXPECTED_SHA; origin/$DEFAULT_BRANCH is $latest_sha."
+ else
+ echo "Mobile upload commit is current for $DEFAULT_BRANCH."
+ fi
+ else
+ echo "Mobile release guard passed for $EVENT_NAME on $RELEASE_BRANCH."
+ fi
+ echo "current=$current" >> "$GITHUB_OUTPUT"
+
- name: Setup Node.js
+ if: steps.release-guard.outputs.current == 'true'
uses: actions/setup-node@v5
with:
node-version: 22
cache: npm
- name: Setup Java
+ if: steps.release-guard.outputs.current == 'true'
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21
- name: Setup Android SDK
+ if: steps.release-guard.outputs.current == 'true'
uses: android-actions/setup-android@v3
- name: Install Android SDK packages
+ if: steps.release-guard.outputs.current == 'true'
shell: bash
run: |
set -euo pipefail
@@ -67,10 +138,11 @@ jobs:
sdkmanager "platforms;android-36" "build-tools;36.0.0"
- name: Resolve mobile version
+ if: steps.release-guard.outputs.current == 'true'
shell: bash
env:
- INPUT_VERSION_NAME: ${{ inputs.version_name }}
- INPUT_VERSION_CODE: ${{ inputs.version_code }}
+ INPUT_VERSION_NAME: ${{ inputs.version_name || '' }}
+ INPUT_VERSION_CODE: ${{ inputs.version_code || '' }}
run: |
set -euo pipefail
version_name="$INPUT_VERSION_NAME"
@@ -84,10 +156,22 @@ jobs:
echo "MOBILE_VERSION_NAME=$version_name" >> "$GITHUB_ENV"
echo "MOBILE_VERSION_CODE=$version_code" >> "$GITHUB_ENV"
+ - name: Check Android store environment
+ if: steps.release-guard.outputs.current == 'true'
+ env:
+ ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
+ ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
+ ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
+ ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
+ GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_BASE64: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_BASE64 }}
+ run: node scripts/mobile/check-store-upload-env.mjs --android
+
- name: Install dependencies
+ if: steps.release-guard.outputs.current == 'true'
run: npm ci --legacy-peer-deps
- name: Decode Android signing key
+ if: steps.release-guard.outputs.current == 'true'
shell: bash
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
@@ -96,10 +180,6 @@ jobs:
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
run: |
set -euo pipefail
- test -n "$ANDROID_KEYSTORE_BASE64"
- test -n "$ANDROID_KEYSTORE_PASSWORD"
- test -n "$ANDROID_KEY_ALIAS"
- test -n "$ANDROID_KEY_PASSWORD"
keystore_path="$RUNNER_TEMP/android-release.keystore"
node -e "const fs = require('fs'); fs.writeFileSync(process.argv[1], Buffer.from(process.env.ANDROID_KEYSTORE_BASE64, 'base64'))" "$keystore_path"
echo "ANDROID_KEYSTORE_FILE=$keystore_path" >> "$GITHUB_ENV"
@@ -108,48 +188,98 @@ jobs:
echo "ANDROID_KEY_PASSWORD=$ANDROID_KEY_PASSWORD" >> "$GITHUB_ENV"
- name: Build and sync Android shell
+ if: steps.release-guard.outputs.current == 'true'
run: |
npm run mobile:android:sync
npm run mobile:permissions:check
npm run mobile:android:signing:check
- name: Build signed Android App Bundle
+ if: steps.release-guard.outputs.current == 'true'
working-directory: android
run: ./gradlew --no-daemon bundleRelease
- name: Verify Android App Bundle signature
- run: jarsigner -verify -certs -verbose android/app/build/outputs/bundle/release/app-release.aab >/dev/null
+ if: steps.release-guard.outputs.current == 'true'
+ run: jarsigner -verify -certs -verbose "$ANDROID_AAB_PATH" >/dev/null
- name: Upload Android artifact
+ if: steps.release-guard.outputs.current == 'true'
uses: actions/upload-artifact@v4
with:
name: truck-wash-android-${{ env.MOBILE_VERSION_NAME }}-${{ github.event.workflow_run.head_sha || github.sha }}
- path: android/app/build/outputs/bundle/release/app-release.aab
+ path: ${{ env.ANDROID_AAB_PATH }}
if-no-files-found: error
retention-days: 14
+ - name: Upload Android App Bundle to Google Play
+ if: steps.release-guard.outputs.current == 'true' && env.UPLOAD_ANDROID_TO_PLAY == 'true'
+ env:
+ GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_BASE64: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_BASE64 }}
+ run: npm run mobile:android:play-upload
+
ios:
- name: iOS IPA
- if: github.event_name != 'workflow_run'
- runs-on: macos-latest
- timeout-minutes: 60
+ name: iOS IPA and App Store upload
+ if: >
+ github.event_name != 'workflow_run' ||
+ (github.event.workflow_run.conclusion == 'success' &&
+ github.event.workflow_run.event == 'push' &&
+ github.event.workflow_run.head_branch == github.event.repository.default_branch)
+ runs-on: macos-15
+ environment: mobile-store-production
+ timeout-minutes: 90
+ env:
+ IOS_PROJECT_PATH: ios/App/App.xcodeproj
+ IOS_SCHEME: App
+ IOS_BUNDLE_ID: io.truckwash.app
+ UPLOAD_IOS_TO_APP_STORE: ${{ github.event_name != 'workflow_dispatch' || inputs.upload_ios_to_app_store }}
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
- ref: ${{ github.sha }}
+ ref: ${{ github.event.workflow_run.head_sha || github.sha }}
+
+ - name: Guard current master release
+ id: release-guard
+ shell: bash
+ env:
+ EVENT_NAME: ${{ github.event_name }}
+ EXPECTED_SHA: ${{ github.event.workflow_run.head_sha || github.sha }}
+ RELEASE_BRANCH: ${{ github.event.workflow_run.head_branch || github.ref_name }}
+ DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
+ run: |
+ set -euo pipefail
+ current=true
+ if [[ "$EVENT_NAME" == "workflow_run" ]]; then
+ latest_sha="$(git ls-remote origin "refs/heads/$DEFAULT_BRANCH" | awk '{print $1}')"
+ if [[ -z "$latest_sha" ]]; then
+ echo "Could not resolve origin/$DEFAULT_BRANCH." >&2
+ exit 1
+ fi
+ if [[ "$latest_sha" != "$EXPECTED_SHA" ]]; then
+ current=false
+ echo "Skipping stale mobile upload for $EXPECTED_SHA; origin/$DEFAULT_BRANCH is $latest_sha."
+ else
+ echo "Mobile upload commit is current for $DEFAULT_BRANCH."
+ fi
+ else
+ echo "Mobile release guard passed for $EVENT_NAME on $RELEASE_BRANCH."
+ fi
+ echo "current=$current" >> "$GITHUB_OUTPUT"
- name: Setup Node.js
+ if: steps.release-guard.outputs.current == 'true'
uses: actions/setup-node@v5
with:
node-version: 22
cache: npm
- name: Resolve mobile version
+ if: steps.release-guard.outputs.current == 'true'
shell: bash
env:
- INPUT_VERSION_NAME: ${{ inputs.version_name }}
- INPUT_VERSION_CODE: ${{ inputs.version_code }}
+ INPUT_VERSION_NAME: ${{ inputs.version_name || '' }}
+ INPUT_VERSION_CODE: ${{ inputs.version_code || '' }}
run: |
set -euo pipefail
version_name="$INPUT_VERSION_NAME"
@@ -163,16 +293,32 @@ jobs:
echo "MOBILE_VERSION_NAME=$version_name" >> "$GITHUB_ENV"
echo "MOBILE_VERSION_CODE=$version_code" >> "$GITHUB_ENV"
+ - name: Check iOS store environment
+ if: steps.release-guard.outputs.current == 'true'
+ env:
+ IOS_CERTIFICATE_BASE64: ${{ secrets.IOS_CERTIFICATE_BASE64 }}
+ IOS_CERTIFICATE_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }}
+ IOS_PROVISION_PROFILE_BASE64: ${{ secrets.IOS_PROVISION_PROFILE_BASE64 }}
+ IOS_KEYCHAIN_PASSWORD: ${{ secrets.IOS_KEYCHAIN_PASSWORD }}
+ APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
+ APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
+ APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
+ APP_STORE_CONNECT_API_PRIVATE_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_API_PRIVATE_KEY_BASE64 }}
+ run: node scripts/mobile/check-store-upload-env.mjs --ios
+
- name: Install dependencies
+ if: steps.release-guard.outputs.current == 'true'
run: npm ci --legacy-peer-deps
- name: Build and sync iOS shell
+ if: steps.release-guard.outputs.current == 'true'
run: |
npm run build
npx cap sync ios
npm run mobile:permissions:check
- name: Install Apple signing assets
+ if: steps.release-guard.outputs.current == 'true'
shell: bash
env:
IOS_CERTIFICATE_BASE64: ${{ secrets.IOS_CERTIFICATE_BASE64 }}
@@ -182,12 +328,6 @@ jobs:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
set -euo pipefail
- test -n "$IOS_CERTIFICATE_BASE64"
- test -n "$IOS_CERTIFICATE_PASSWORD"
- test -n "$IOS_PROVISION_PROFILE_BASE64"
- test -n "$IOS_KEYCHAIN_PASSWORD"
- test -n "$APPLE_TEAM_ID"
-
certificate_path="$RUNNER_TEMP/apple-distribution.p12"
profile_path="$RUNNER_TEMP/app-store.mobileprovision"
keychain_path="$RUNNER_TEMP/app-signing.keychain-db"
@@ -214,14 +354,35 @@ jobs:
echo "IOS_PROFILE_UUID=$profile_uuid" >> "$GITHUB_ENV"
echo "IOS_PROFILE_NAME=$profile_name" >> "$GITHUB_ENV"
+ - name: Install App Store Connect API key
+ if: steps.release-guard.outputs.current == 'true' && env.UPLOAD_IOS_TO_APP_STORE == 'true'
+ shell: bash
+ env:
+ APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
+ APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
+ APP_STORE_CONNECT_API_PRIVATE_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_API_PRIVATE_KEY_BASE64 }}
+ run: |
+ set -euo pipefail
+ private_keys_dir="$RUNNER_TEMP/private_keys"
+ private_key_path="$private_keys_dir/AuthKey_${APP_STORE_CONNECT_API_KEY_ID}.p8"
+ mkdir -p "$private_keys_dir"
+ node -e "const fs = require('fs'); fs.writeFileSync(process.argv[1], Buffer.from(process.env.APP_STORE_CONNECT_API_PRIVATE_KEY_BASE64, 'base64'))" "$private_key_path"
+ chmod 600 "$private_key_path"
+ echo "API_PRIVATE_KEYS_DIR=$private_keys_dir" >> "$GITHUB_ENV"
+ echo "APP_STORE_CONNECT_API_KEY_ID=$APP_STORE_CONNECT_API_KEY_ID" >> "$GITHUB_ENV"
+ echo "APP_STORE_CONNECT_ISSUER_ID=$APP_STORE_CONNECT_ISSUER_ID" >> "$GITHUB_ENV"
+ echo "APP_STORE_CONNECT_API_KEY_PATH=$private_key_path" >> "$GITHUB_ENV"
+
- name: Resolve Swift packages
- run: xcodebuild -resolvePackageDependencies -project ios/App/App.xcodeproj -scheme App
+ if: steps.release-guard.outputs.current == 'true'
+ run: xcodebuild -resolvePackageDependencies -project "$IOS_PROJECT_PATH" -scheme "$IOS_SCHEME"
- name: Archive iOS app
+ if: steps.release-guard.outputs.current == 'true'
run: |
xcodebuild \
- -project ios/App/App.xcodeproj \
- -scheme App \
+ -project "$IOS_PROJECT_PATH" \
+ -scheme "$IOS_SCHEME" \
-configuration Release \
-destination "generic/platform=iOS" \
-archivePath "$RUNNER_TEMP/TruckWash.xcarchive" \
@@ -234,6 +395,7 @@ jobs:
CURRENT_PROJECT_VERSION="$MOBILE_VERSION_CODE"
- name: Export iOS IPA
+ if: steps.release-guard.outputs.current == 'true'
shell: bash
run: |
set -euo pipefail
@@ -255,7 +417,7 @@ jobs:
+
+
+
+
diff --git a/src/components/buefy/tree/BuefyTreeNode.vue b/src/components/buefy/tree/BuefyTreeNode.vue
new file mode 100644
index 00000000..864e0529
--- /dev/null
+++ b/src/components/buefy/tree/BuefyTreeNode.vue
@@ -0,0 +1,196 @@
+
+
+
+
+
+
{{ $t("system_status.sections.modules") }}
+ ++ {{ systemStatusErrorMessage() }} +
+{{ moduleLabel(module.key) }}
+{{ moduleLabel(module.key) }}
+ + + {{ $t("system_status.labels.version") }}: {{ moduleVersion(module) }} + + ++
{{ moduleReasonText(module) }}
-