Skip to content

Test Mobile E2E

Test Mobile E2E #8

name: "Test Mobile E2E"
on:
schedule:
- cron: "0 3 * * 1-5"
workflow_call:
workflow_dispatch:
inputs:
ref:
description: |
If you run this manually, and want to run on a PR, the correct ref should be refs/pull/{PR_NUMBER}/merge to
have the "normal" scenario involving checking out a merge commit between your branch and the base branch.
If you want to run only on a branch or specific commit, you can use either the sha or the branch name instead (prefer the first verion for PRs).
type: string
required: false
login:
description: The GitHub username that triggered the workflow
type: string
required: false
base_ref:
description: The base branch to merge the head into when checking out the code
type: string
required: false
export_to_xray:
description: Send tests results to Xray
required: false
type: boolean
default: false
test_execution_android:
description: "[Android] Test Execution ticket ID. Ex: 'B2CQA-2461'"
required: false
type: string
test_execution_ios:
description: "[iOS] Test Execution ticket ID. Ex: 'B2CQA-2461'"
required: false
type: string
speculos_tests:
description: Run Speculos tests
required: false
type: boolean
default: false
slack_notif:
description: "Send notification to Slack?"
required: false
type: boolean
default: false
# Uncomment to have log-level: trace on detox run and build
# (cf: apps/ledger-live-mobile/detox.config.js)
# env:
# DEBUG_DETOX: true
permissions:
id-token: write
contents: read
env:
SPECULOS_IMAGE_TAG: ghcr.io/ledgerhq/speculos:0.11
COINAPPS: ${{ github.workspace }}/coin-apps
SPECULOS_RUN: ${{ inputs.speculos_tests || github.event_name == 'schedule' }}
jobs:
detox-tests-ios:
name: "LLM - iOS Detox Tests"
runs-on: [m1, ARM64]
if: ${{ !inputs.speculos_tests && github.event_name != 'schedule' }}
env:
NODE_OPTIONS: "--max-old-space-size=7168"
LANG: en_US.UTF-8
LANGUAGE: en_US.UTF-8
LC_ALL: en_US.UTF-8
outputs:
status: ${{ steps.detox.outcome }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.sha }}
- name: setup caches
id: caches
uses: LedgerHQ/ledger-live/tools/actions/composites/setup-caches@develop
with:
skip-pod-cache: "false"
skip-turbo-cache: "false"
accountId: ${{ secrets.AWS_ACCOUNT_ID_PROD }}
roleName: ${{ secrets.AWS_CACHE_ROLE_NAME }}
region: ${{ secrets.AWS_CACHE_REGION }}
turbo-server-token: ${{ secrets.TURBOREPO_SERVER_TOKEN }}
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
id: aws
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID_PROD }}:role/${{ secrets.AWS_CACHE_ROLE_NAME }}
aws-region: ${{ secrets.AWS_CACHE_REGION }}
- uses: nick-fields/retry@v3
name: install dependencies
with:
max_attempts: 2
timeout_minutes: 15
command: pnpm i --filter="live-mobile..." --filter="ledger-live" --filter="@ledgerhq/dummy-*-app..." --no-frozen-lockfile --unsafe-perm
new_command_on_retry: rm -rf ~/.cocoapods/ && pnpm clean && pnpm i --filter="live-mobile..." --filter="ledger-live" --filter="@ledgerhq/dummy-*-app..." --no-frozen-lockfile --unsafe-perm
- name: cache detox build
uses: tespkg/actions-cache@v1
if: steps.aws.conclusion == 'success'
id: detox-build
with:
path: ${{ github.workspace }}/apps/ledger-live-mobile/ios/build/Build/Products/Release-iphonesimulator
key: ${{ runner.os }}-detox-${{ hashFiles('apps/ledger-live-mobile/ios/Podfile.lock', 'apps/ledger-live-mobile/ios/ledgerlivemobile.xcodeproj/project.pbxproj', 'apps/ledger-live-mobile/detox.config.js', 'apps/ledger-live-mobile/.env.mock') }}
accessKey: ${{ env.AWS_ACCESS_KEY_ID }}
secretKey: ${{ env.AWS_SECRET_ACCESS_KEY }}
sessionToken: ${{ env.AWS_SESSION_TOKEN}}
bucket: ll-gha-s3-cache
region: ${{ secrets.AWS_CACHE_REGION }}
use-fallback: false
- name: Build dependencies
run: |
pnpm build:llm:deps --api="http://127.0.0.1:${{ steps.caches.outputs.port }}" --token="${{ secrets.TURBOREPO_SERVER_TOKEN }}" --team="foo"
- name: Build Dummy Live SDK and Dummy Wallet API apps for testing
run: |
pnpm build:dummy-apps
shell: bash
- name: Create iOS simulator
id: simulator
run: |
ID=$(xcrun simctl create "iPhone 14" "iPhone 14")
echo "id=$ID" >> $GITHUB_OUTPUT
- name: Build iOS app for Detox test run
if: steps.detox-build.outputs.cache-hit != 'true'
run: pnpm mobile e2e:ci -p ios -b
- name: Build JS Bundle app for Detox test run
if: steps.detox-build.outputs.cache-hit == 'true'
run: pnpm mobile e2e:ci -p ios --bundle
- name: Setup Speculos image and Coin Apps
if: ${{ env.SPECULOS_RUN == 'true' }}
uses: LedgerHQ/ledger-live/tools/actions/composites/setup-speculos_image@develop
with:
coinapps_path: ${{ env.COINAPPS }}
speculos_tag: ${{ env.SPECULOS_IMAGE_TAG }}
bot_id: ${{ secrets.GH_BOT_APP_ID }}
bot_key: ${{ secrets.GH_BOT_PRIVATE_KEY }}
- name: Test iOS app
id: detox
timeout-minutes: 75
run: pnpm mobile e2e:ci -p ios -t $([[ "$INPUT_SPECULOS" == "true" ]] && printf %s '--speculos')
env:
SEED: ${{ secrets.SEED_QAA_B2C }}
INPUT_SPECULOS: ${{ env.SPECULOS_RUN }}
- name: Delete iOS simulator
if: ${{ always() && steps.simulator.outputs.id }}
run: |
xcrun simctl delete ${{ steps.simulator.outputs.id }}
- name: Upload test artifacts
uses: actions/upload-artifact@v4
if: ${{ !cancelled() || steps.detox.outcome == 'cancelled' }}
with:
name: "ios-test-artifacts"
path: apps/ledger-live-mobile/artifacts
allure-report-ios:
name: "Allure Reports Export on Server"
runs-on: [ledger-live-medium]
if: ${{ always() && (inputs.slack_notif || github.event_name == 'push') }}
needs: [detox-tests-ios]
outputs:
report-url: ${{ steps.upload.outputs.report-url }}
result: ${{ steps.summary.outputs.test_result }}
status: ${{ needs.detox-tests-ios.outputs.status }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ ((github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call' || github.event_name == 'pull_request') && (inputs.ref || github.ref_name)) || github.sha }}
- uses: LedgerHQ/ledger-live/tools/actions/composites/upload-allure-report@develop
id: upload
with:
platform: ios
login: ${{ vars.ALLURE_USERNAME }}
password: ${{ secrets.ALLURE_LEDGER_LIVE_PASSWORD }}
path: ios-test-artifacts
- name: Get summary
id: summary
if: ${{ !cancelled() }}
uses: LedgerHQ/ledger-live/tools/actions/composites/get-allure-summary@develop
with:
allure-results-path: ios-test-artifacts
platform: iOS
detox-tests-android:
name: "Ledger Live Mobile - Android Detox Tests"
runs-on: [ledger-live-linux-8CPU-32RAM]
env:
NODE_OPTIONS: "--max-old-space-size=7168"
LANG: en_US.UTF-8
LANGUAGE: en_US.UTF-8
LC_ALL: en_US.UTF-8
AVD_API: 32
AVD_ARCH: x86_64
AVD_PROFILE: pixel_6_pro
AVD_TARGET: google_apis
AVD_NAME: "Pixel_6_Pro_API_32"
AVD_CORES: 4
AVD_RAM_SIZE: 4096M
AVD_OPTIONS: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
outputs:
status: ${{ steps.detox.outcome }}
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3]
shardTotal: [3, 1]
exclude:
- shardIndex: ${{ (github.event.inputs.speculos_tests == 'false' || github.event_name == 'push') && '2' }}
- shardIndex: ${{ (github.event.inputs.speculos_tests == 'false' || github.event_name == 'push') && '3' }}
- shardTotal: ${{ (github.event.inputs.speculos_tests == 'false' || github.event_name == 'push') && '3' }}
- shardTotal: ${{ github.event.inputs.speculos_tests != 'false' && github.event_name != 'push' && '1' }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.sha }}
- name: Setup the caches
uses: LedgerHQ/ledger-live/tools/actions/composites/setup-caches@develop
id: setup-caches
with:
install-proto: true
skip-turbo-cache: "false"
accountId: ${{ secrets.AWS_ACCOUNT_ID_PROD }}
roleName: ${{ secrets.AWS_CACHE_ROLE_NAME }}
region: ${{ secrets.AWS_CACHE_REGION }}
turbo-server-token: ${{ secrets.TURBOREPO_SERVER_TOKEN }}
- name: setup JDK 17
uses: actions/setup-java@v3
with:
distribution: "zulu"
java-version: "17"
cache: "gradle"
- name: setup Android SDK
uses: android-actions/[email protected]
- name: Gradle cache
uses: gradle/gradle-build-action@v2
# https://github.blog/changelog/2023-02-23-hardware-accelerated-android-virtualization-on-actions-windows-and-linux-larger-hosted-runners/
- name: Enable KVM group perms
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: Fix emulator directory permissions
continue-on-error: true
run: sudo chown -R $(whoami):$(id -ng) /usr/local/lib/android/sdk/emulator/
- name: Install dependencies
run: |
pnpm i --filter="live-mobile..." --filter="ledger-live" --filter="@ledgerhq/dummy-*-app..." --no-frozen-lockfile --unsafe-perm
- name: Build dependencies
run: |
pnpm build:llm:deps --api="http://127.0.0.1:${{ steps.setup-caches.outputs.port }}" --token="${{ secrets.TURBOREPO_SERVER_TOKEN }}" --team="foo"
- name: Build Dummy Live SDK and Dummy Wallet API apps for testing
run: |
pnpm build:dummy-apps --api="http://127.0.0.1:${{ steps.setup-caches.outputs.port }}" --token="${{ secrets.TURBOREPO_SERVER_TOKEN }}" --team="foo"
shell: bash
- name: Build Android app for Detox test run
run: |
pnpm mobile e2e:ci -p android -b
- name: cache android emulator
timeout-minutes: 5
uses: tespkg/actions-cache@v1
id: detox-avd
continue-on-error: true
with:
path: |
~/.android/avd/*
~/.android/adb*
/usr/local/lib/android/sdk/system-images/android-${{ env.AVD_API }}/${{ env.AVD_TARGET }}/${{ env.AVD_ARCH }}/*
/usr/local/lib/android/sdk/emulator/*
key: ${{ runner.os }}-detox-avd-${{ env.AVD_NAME }}-${{ env.AVD_PROFILE }}-${{ env.AVD_TARGET }}-${{ env.AVD_API }}-${{ env.AVD_ARCH }}
accessKey: ${{ env.AWS_ACCESS_KEY_ID }}
secretKey: ${{ env.AWS_SECRET_ACCESS_KEY }}
sessionToken: ${{ env.AWS_SESSION_TOKEN }}
bucket: ll-gha-s3-cache
region: ${{ secrets.AWS_CACHE_REGION }}
use-fallback: false
- name: create AVD and generate snapshot for caching
if: steps.detox-avd.outputs.cache-hit != 'true'
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ env.AVD_API }}
arch: ${{ env.AVD_ARCH }}
profile: ${{ env.AVD_PROFILE }}
target: ${{ env.AVD_TARGET }}
avd-name: ${{ env.AVD_NAME }}
force-avd-creation: true
cores: ${{ env.AVD_CORES }}
ram-size: ${{ env.AVD_RAM_SIZE }}
disable-linux-hw-accel: false
emulator-options: ${{ env.AVD_OPTIONS }}
script: ./tools/scripts/wait_emulator_idle.sh
- name: Setup Speculos image and Coin Apps
if: ${{ env.SPECULOS_RUN == 'true' }}
uses: LedgerHQ/ledger-live/tools/actions/composites/setup-speculos_image@develop
with:
coinapps_path: ${{ env.COINAPPS }}
speculos_tag: ${{ env.SPECULOS_IMAGE_TAG }}
bot_id: ${{ secrets.GH_BOT_APP_ID }}
bot_key: ${{ secrets.GH_BOT_PRIVATE_KEY }}
- name: Run Android Tests
id: detox
run: pnpm mobile e2e:ci -p android -t $([[ "$INPUT_SPECULOS" == "true" ]] && printf %s '--speculos') --shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
timeout-minutes: ${{ env.SPECULOS_RUN && 120 || 45 }}
env:
DETOX_INSTALL_TIMEOUT: 120000
SEED: ${{ secrets.SEED_QAA_B2C }}
INPUT_SPECULOS: ${{ env.SPECULOS_RUN }}
- name: Upload test artifacts
uses: actions/upload-artifact@v4
if: ${{ !cancelled() || steps.detox.outcome == 'cancelled' }}
with:
name: "android-test-artifacts-${{ matrix.shardIndex }}"
path: apps/ledger-live-mobile/artifacts/
allure-report-android:
name: "Allure Reports Export on Server"
runs-on: [ledger-live-medium]
if: ${{ always() && (inputs.slack_notif || github.event_name == 'push' || github.event_name == 'schedule') }}
outputs:
report-url: ${{ steps.upload.outputs.report-url }}
result: ${{ steps.summary.outputs.test_result }}
status: ${{ needs.detox-tests-android.outputs.status }}
needs: [detox-tests-android]
steps:
- name: checkout
uses: actions/checkout@v4
with:
ref: ${{ ((github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call' || github.event_name == 'pull_request') && (inputs.ref || github.ref_name)) || github.sha }}
- uses: LedgerHQ/ledger-live/tools/actions/composites/upload-allure-report@develop
id: upload
with:
platform: android
login: ${{ vars.ALLURE_USERNAME }}
password: ${{ secrets.ALLURE_LEDGER_LIVE_PASSWORD }}
path: android-test-artifacts
- name: Get summary
id: summary
if: ${{ !cancelled() }}
uses: LedgerHQ/ledger-live/tools/actions/composites/get-allure-summary@develop
with:
allure-results-path: android-test-artifacts
platform: android
upload-to-xray:
name: "Upload to Xray"
runs-on: [ledger-live-medium]
strategy:
matrix:
platform:
- android
- ios
fail-fast: false
env:
XRAY_CLIENT_ID: ${{ secrets.XRAY_CLIENT_ID }}
XRAY_CLIENT_SECRET: ${{ secrets.XRAY_CLIENT_SECRET }}
XRAY_API_URL: https://xray.cloud.getxray.app/api/v2
JIRA_URL: https://ledgerhq.atlassian.net/browse
TEST_EXECUTION: ${{ matrix.platform == 'android' && inputs.test_execution_android || inputs.test_execution_ios }}
needs: [detox-tests-android, detox-tests-ios]
if: ${{ !cancelled() && inputs.export_to_xray }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.sha }}
- name: Download Allure Results
uses: actions/download-artifact@v4
with:
path: "artifacts-${{ matrix.platform }}"
name: ${{ matrix.platform }}-test-artifacts
- name: Format Xray results
run: apps/ledger-live-mobile/e2e/xray.formater.sh artifacts-${{ matrix.platform }} ${{ matrix.platform }} ${{ env.TEST_EXECUTION}}
- name: Upload aggregated xray results
uses: actions/upload-artifact@v4
with:
retention-days: 1
name: xray-reports-${{ matrix.platform }}
path: "artifacts-${{ matrix.platform }}/xray_report.json"
- name: Authenticate to Xray
id: authenticate
run: |
response=$(curl -H "Content-Type: application/json" -X POST --data '{"client_id": "${{ env.XRAY_CLIENT_ID }}", "client_secret": "${{ env.XRAY_CLIENT_SECRET }}"}' ${{ env.XRAY_API_URL }}/authenticate)
echo "xray_token=$response" >> $GITHUB_OUTPUT
- name: Publish report on Xray
id: publish-xray
run: |
response=$(curl -H "Content-Type: application/json" \
-H "Authorization: Bearer ${{ steps.authenticate.outputs.xray_token }}" \
-X POST \
--data @artifacts-${{ matrix.platform }}/xray_report.json \
${{ env.XRAY_API_URL }}/import/execution)
key=$(echo $response | jq -r '.key')
echo "xray_key=$key" >> $GITHUB_OUTPUT
- name: Write Xray report link in summary
shell: bash
run: echo "::notice title=${{ matrix.platform }} Xray report URL::${{ env.JIRA_URL }}/${{ steps.publish-xray.outputs.xray_key }}"
report-on-slack:
runs-on: ubuntu-22.04
needs: [allure-report-android, allure-report-ios]
if: ${{ (failure() && github.event_name == 'push') || (always() && (inputs.slack_notif || github.event_name == 'schedule')) }}
env:
IOS_STATUS: ${{ needs.allure-report-ios.outputs.status }}
IOS_REPORT_URL: ${{ needs.allure-report-ios.outputs.report-url }}
ANDROID_STATUS: ${{ needs.allure-report-android.outputs.status }}
ANDROID_REPORT_URL: ${{ needs.allure-report-android.outputs.report-url }}
steps:
- name: format message
uses: actions/github-script@v7
id: message
with:
script: |
const fs = require("fs");
const text = "Ledger Live Mobile E2E tests finished";
const header = [
{
"type": "header",
"text": {
"type": "plain_text",
"text": process.env.SPECULOS_RUN == 'false'
? "Ledger Live Mobile Mocked Tests on ${{ github.ref_name }}"
: ":ledger-logo: Ledger Live Mobile E2E nightly tests results on ${{ github.ref_name }}",
"emoji": true
}
},
{
"type": "divider"
}
];
const iOSResult = [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": `- 🍏 iOS: ${process.env.IOS_STATUS !== 'success' ? '❌' : '✅'} ${{ needs.allure-report-ios.outputs.result || 'No test results' }}`
}
}
];
const androidResult = [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": `- 🤖 Android: ${process.env.ANDROID_STATUS !== 'success' ? '❌' : '✅'} ${{ needs.allure-report-android.outputs.result || 'No test results' }}`
}
}
];
const iOSInfo = [
{
"type": "mrkdwn",
"text": process.env.IOS_REPORT_URL ? `*Allure Report iOS*\n<${process.env.IOS_REPORT_URL}|Allure Report iOS>` : '*Allure Report iOS*\nNo Allure Report'
}
];
const androidInfo = [
{
"type": "mrkdwn",
"text": process.env.ANDROID_REPORT_URL ? `*Allure Report Android*\n<${process.env.ANDROID_REPORT_URL}|Allure Report Android>` : '*Allure Report Android*\nNo Allure Report'
}
];
const infoFields = []
.concat(${{ env.SPECULOS_RUN == 'true' }} ? [] : iOSInfo)
.concat(androidInfo)
.concat([
{
"type": "mrkdwn",
"text": `*Workflow*\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Workflow run>`
}
]);
const infoBlock = [
{
"type": "divider"
},
{
"type": "section",
"fields": infoFields
}
];
const blocks = []
.concat(header)
.concat(${{ env.SPECULOS_RUN == 'true' }} ? [] : iOSResult)
.concat(androidResult)
.concat(infoBlock);
const result = process.env.SPECULOS_RUN === 'false'
? {
text,
blocks,
}
: {
attachments: [
{
color: process.env.ANDROID_STATUS !== 'success'
? "#FF333C"
: "#33FF39",
blocks,
},
],
};
fs.writeFileSync(`./payload-slack-content.json`, JSON.stringify(result, null, 2));
- name: post to a Slack channel
id: slack
uses: slackapi/[email protected]
with:
channel-id: "CTMQ0S5SB"
payload-file-path: "./payload-slack-content.json"
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_USER_OAUTH_ACCESS_TOKEN }}
- name: post to a Slack channel
if: ${{ github.event_name == 'push' && contains(fromJson('["develop", "main"]'), github.ref_name) || github.event_name == 'schedule' }}
uses: slackapi/[email protected]
with:
channel-id: "C05FKJ7DFAP"
payload-file-path: "./payload-slack-content.json"
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_USER_OAUTH_ACCESS_TOKEN }}