Skip to content

QA Android UI Tests #41

QA Android UI Tests

QA Android UI Tests #41

name: QA Android UI Tests
on:
workflow_dispatch:
inputs:
flavor:
description: "App flavor."
required: true
type: choice
options:
- internal release candidate
- internal beta
- staging compat
- experimental
- column-1
- column-2
- column-3
- debug
- fdroid
- production
default: internal release candidate
appBuildNumber:
description: "Use 'latest' OR a build number substring (e.g. 71389) OR an .apk key/filename"
required: true
default: "latest"
type: string
isUpgrade:
description: "Upgrade test? If true, oldBuildNumber is REQUIRED."
required: true
default: false
type: boolean
oldBuildNumber:
description: "For upgrade runs: old build number substring (or an .apk filename)"
required: false
default: ""
type: string
enforceAppInstall:
description: "Allow downgrade install (-d). Use true if device has higher version."
required: true
default: false
type: boolean
TAGS:
description: "Tags: '@regression' OR '@TC-8143'. (First non-empty wins.)"
required: false
default: ""
type: string
androidDeviceId:
description: "Target a specific device. Leave empty to use ALL connected devices."
required: false
default: ""
type: string
permissions:
contents: read
jobs:
validate-and-resolve-inputs:
name: Validate + resolve selectors
runs-on: ubuntu-latest
outputs:
resolvedTestCaseId: ${{ steps.resolve_selector.outputs.testCaseId }}
resolvedCategory: ${{ steps.resolve_selector.outputs.category }}
s3Folder: ${{ steps.resolve_flavor.outputs.s3Folder }}
appId: ${{ steps.resolve_flavor.outputs.appId }}
steps:
- name: Validate upgrade inputs
shell: bash
run: |
set -euo pipefail
if [[ "${{ inputs.isUpgrade }}" == "true" && -z "${{ inputs.oldBuildNumber }}" ]]; then
echo "ERROR: oldBuildNumber is REQUIRED when isUpgrade=true"
exit 1
fi
- name: Resolve selector from TAGS
id: resolve_selector
shell: bash
run: |
set -euo pipefail
TESTCASE_ID=""
CATEGORY=""
TAGS_RAW="${{ inputs.TAGS }}"
trim() { echo "$1" | xargs; }
if [[ -n "$(trim "${TAGS_RAW}")" ]]; then
sel=""
IFS=',' read -ra parts <<< "${TAGS_RAW}"
for p in "${parts[@]}"; do
t="$(trim "$p")"
if [[ -n "$t" ]]; then
sel="$t"
break
fi
done
sel="${sel#@}"
sel="$(trim "$sel")"
if [[ "$sel" == *:* ]]; then
echo "ERROR: TAGS format '@key:value' is not supported yet. Use '@TC-1234' or '@category'."
exit 1
fi
if [[ "$sel" =~ ^TC-[0-9]+$ ]]; then
TESTCASE_ID="$sel"
else
CATEGORY="$sel"
fi
fi
echo "testCaseId=$TESTCASE_ID" >> "$GITHUB_OUTPUT"
echo "category=$CATEGORY" >> "$GITHUB_OUTPUT"
- name: Resolve flavor -> S3_FOLDER + APP_ID
id: resolve_flavor
shell: bash
run: |
set -euo pipefail
FLAVOR="$(echo "${{ inputs.flavor }}" | xargs)"
S3_FOLDER=""
APP_ID=""
case "$FLAVOR" in
"staging compat")
S3_FOLDER="artifacts/megazord/android/reloaded/staging/compat/"
APP_ID="com.waz.zclient.dev"
;;
"internal beta")
S3_FOLDER="artifacts/megazord/android/reloaded/beta/release/"
APP_ID="com.wire.android.internal"
;;
"internal release candidate")
S3_FOLDER="artifacts/megazord/android/reloaded/internal/compat/"
APP_ID="com.wire.internal"
;;
"experimental")
S3_FOLDER="artifacts/megazord/android/reloaded/staging/compat/"
APP_ID="com.waz.zclient.dev"
;;
"column-1")
S3_FOLDER="android/custom/bund/column1/prod/compatrelease/"
APP_ID="com.wire.android.bund"
;;
"column-2")
S3_FOLDER="android/custom/bund/column2/prod/compatrelease/"
APP_ID="com.wire.android.bund.column2"
;;
"column-3")
S3_FOLDER="android/custom/bund/column3/prod/compatrelease/"
APP_ID="com.wire.android.bund.column3"
;;
"debug")
S3_FOLDER="artifacts/megazord/android/reloaded/dev/debug/"
APP_ID="com.waz.zclient.dev.debug"
;;
"fdroid")
S3_FOLDER="artifacts/megazord/android/reloaded/fdroid/compatrelease/"
APP_ID="com.wire"
;;
"production")
S3_FOLDER="artifacts/megazord/android/reloaded/prod/compatrelease/"
APP_ID="com.wire"
;;
*)
echo "ERROR: Unknown flavor: '$FLAVOR'"
exit 1
;;
esac
echo "s3Folder=$S3_FOLDER" >> "$GITHUB_OUTPUT"
echo "appId=$APP_ID" >> "$GITHUB_OUTPUT"
- name: Print resolved values
shell: bash
run: |
set -euo pipefail
echo "flavor=${{ inputs.flavor }}"
echo "S3_FOLDER=${{ steps.resolve_flavor.outputs.s3Folder }}"
echo "APP_ID=${{ steps.resolve_flavor.outputs.appId }}"
echo "resolvedTestCaseId=${{ steps.resolve_selector.outputs.testCaseId }}"
echo "resolvedCategory=${{ steps.resolve_selector.outputs.category }}"
run-android-ui-tests:
name: Run Android UI tests
runs-on:
- self-hosted
- Linux
- X64
- office
- android-qa
needs: validate-and-resolve-inputs
env:
AWS_REGION: eu-west-1
S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
OP_VAULT: "Test Automation"
S3_FOLDER: ${{ needs.validate-and-resolve-inputs.outputs.s3Folder }}
APP_ID: ${{ needs.validate-and-resolve-inputs.outputs.appId }}
defaults:
run:
shell: bash
steps:
- name: Checkout (with submodules)
uses: actions/checkout@v4
with:
clean: true
submodules: recursive
- name: Set up Java 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "17"
cache: gradle
- name: Set up Android SDK (ANDROID_HOME + adb)
uses: android-actions/setup-android@v3
- name: Ensure required tools exist
run: |
set -euo pipefail
command -v adb >/dev/null 2>&1 || { echo "ERROR: adb not found"; exit 1; }
command -v python3 >/dev/null 2>&1 || { echo "ERROR: python3 not found on this runner"; exit 1; }
if command -v aws >/dev/null 2>&1; then
aws --version
exit 0
fi
command -v curl >/dev/null 2>&1 || { echo "ERROR: curl not found"; exit 1; }
command -v unzip >/dev/null 2>&1 || { echo "ERROR: unzip not found"; exit 1; }
: "${RUNNER_TEMP:?RUNNER_TEMP not set}"
echo "aws CLI not found. Installing AWS CLI v2 locally..."
AWS_ROOT="${RUNNER_TEMP}/awscli"
ZIP_PATH="${RUNNER_TEMP}/awscliv2.zip"
rm -rf "${AWS_ROOT}" "${ZIP_PATH}" "${RUNNER_TEMP}/aws"
mkdir -p "${AWS_ROOT}"
curl -fsSL -o "${ZIP_PATH}" "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip"
unzip -oq "${ZIP_PATH}" -d "${RUNNER_TEMP}"
rm -f "${ZIP_PATH}"
"${RUNNER_TEMP}/aws/install" -i "${AWS_ROOT}" -b "${AWS_ROOT}/bin"
echo "${AWS_ROOT}/bin" >> "${GITHUB_PATH}"
export PATH="${AWS_ROOT}/bin:${PATH}"
aws --version
- name: Configure AWS credentials (for S3)
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: eu-west-1
- name: Download APK(s) from S3
id: download_apks
env:
APP_BUILD_NUMBER: ${{ inputs.appBuildNumber }}
IS_UPGRADE: ${{ inputs.isUpgrade }}
OLD_BUILD_NUMBER: ${{ inputs.oldBuildNumber }}
run: |
set -euo pipefail
: "${S3_BUCKET:?ERROR: Missing secret AWS_S3_BUCKET}"
: "${S3_FOLDER:?ERROR: S3_FOLDER missing}"
echo "S3 prefix: s3://${S3_BUCKET}/${S3_FOLDER}"
aws s3api list-objects-v2 \
--bucket "${S3_BUCKET}" \
--prefix "${S3_FOLDER}" \
--query "Contents[?ends_with(Key, '.apk')].Key" \
--output json > "${RUNNER_TEMP}/apk_keys.json"
python3 - <<'PY' | tee "${RUNNER_TEMP}/apk_env.txt"
import json, os, re, sys
keys_path = os.path.join(os.environ["RUNNER_TEMP"], "apk_keys.json")
try:
data = json.load(open(keys_path, "r", encoding="utf-8"))
except Exception:
data = []
if not isinstance(data, list):
data = []
apks = [k for k in data if isinstance(k, str) and k.lower().endswith(".apk")]
if not apks:
print("ERROR: No .apk files found in this prefix.", file=sys.stderr)
sys.exit(1)
app_build = (os.environ.get("APP_BUILD_NUMBER") or "").strip()
is_upgrade = (os.environ.get("IS_UPGRADE", "false").strip().lower() == "true")
old_input = (os.environ.get("OLD_BUILD_NUMBER") or "").strip()
def parse_version(fname: str):
m = re.search(r"-v(\d+)\.(\d+)\.(\d+)-(\d+)", fname)
if m:
return (int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4)))
m = re.search(r"-v(\d+)\.(\d+)\.(\d+)-fdroid", fname)
if m:
return (int(m.group(1)), int(m.group(2)), int(m.group(3)), 0)
m = re.search(r"-v(\d+)\.(\d+)\.(\d+)", fname)
if m:
return (int(m.group(1)), int(m.group(2)), int(m.group(3)), 0)
return None
def build_label(fname: str):
m = re.search(r"-v(\d+\.\d+\.\d+-\d+)", fname)
if m: return m.group(1)
m = re.search(r"-v(\d+\.\d+\.\d+)-fdroid", fname)
if m: return m.group(1)
m = re.search(r"-v(\d+\.\d+\.\d+)", fname)
if m: return m.group(1)
return ""
def pick_by_substring(substr: str):
if not substr:
return None
for k in apks:
if substr in k.split("/")[-1]:
return k
return None
def pick_by_filename(filename: str):
if not filename:
return None
for k in apks:
if k.split("/")[-1] == filename:
return k
return None
parsed = []
for k in apks:
pv = parse_version(k.split("/")[-1])
if pv is not None:
parsed.append((pv, k))
parsed.sort(key=lambda x: x[0])
latest_key = parsed[-1][1] if parsed else apks[-1]
second_latest_key = parsed[-2][1] if len(parsed) >= 2 else None
def normalize_direct(s: str):
s = s.strip()
if s.startswith("s3://"):
# s3://bucket/key...
parts = s.split("/", 3)
return parts[3] if len(parts) >= 4 else ""
return s.lstrip("/")
new_key = None
old_key = None
# Direct .apk input: allow s3://..., key..., or filename.apk
if app_build.lower().endswith(".apk"):
direct = normalize_direct(app_build)
if "/" in direct:
new_key = direct
else:
new_key = pick_by_filename(direct) or pick_by_substring(direct)
if is_upgrade:
if old_input.lower().endswith(".apk"):
od = normalize_direct(old_input)
old_key = (od if "/" in od else (pick_by_filename(od) or pick_by_substring(od)))
else:
old_key = pick_by_substring(old_input) if old_input else second_latest_key
elif app_build == "latest":
new_key = latest_key
if is_upgrade:
old_key = pick_by_substring(old_input) if old_input else second_latest_key
else:
new_key = pick_by_substring(app_build)
if is_upgrade:
if not old_input:
print("ERROR: isUpgrade=true but oldBuildNumber is empty.", file=sys.stderr)
sys.exit(1)
old_key = pick_by_substring(old_input)
if not new_key:
print(f"ERROR: Could not resolve NEW apk for appBuildNumber='{app_build}'", file=sys.stderr)
sys.exit(1)
if is_upgrade and not old_key:
print("ERROR: Upgrade requested but OLD apk could not be resolved.", file=sys.stderr)
sys.exit(1)
new_name = new_key.split("/")[-1]
old_name = old_key.split("/")[-1] if old_key else ""
print(f"NEW_S3_KEY={new_key}")
print(f"OLD_S3_KEY={old_key or ''}")
print(f"NEW_APK_NAME={new_name}")
print(f"OLD_APK_NAME={old_name}")
print(f"REAL_BUILD_NUMBER={build_label(new_name)}")
print(f"OLD_BUILD_NUMBER={build_label(old_name) if old_name else ''}")
PY
# Make vars available now + future steps
set -a
source "${RUNNER_TEMP}/apk_env.txt"
set +a
cat "${RUNNER_TEMP}/apk_env.txt" >> "$GITHUB_ENV"
cat "${RUNNER_TEMP}/apk_env.txt" >> "$GITHUB_OUTPUT"
NEW_APK_PATH="${RUNNER_TEMP}/Wire.apk"
echo "NEW_APK_PATH=${NEW_APK_PATH}" >> "$GITHUB_ENV"
aws s3 cp "s3://${S3_BUCKET}/${NEW_S3_KEY}" "${NEW_APK_PATH}" --only-show-errors
test -s "${NEW_APK_PATH}"
if [[ "${IS_UPGRADE}" == "true" ]]; then
OLD_APK_PATH="${RUNNER_TEMP}/Wire.old.apk"
echo "OLD_APK_PATH=${OLD_APK_PATH}" >> "$GITHUB_ENV"
aws s3 cp "s3://${S3_BUCKET}/${OLD_S3_KEY}" "${OLD_APK_PATH}" --only-show-errors
test -s "${OLD_APK_PATH}"
fi
echo "Selected NEW=${NEW_APK_NAME}"
if [[ "${IS_UPGRADE}" == "true" ]]; then
echo "Selected OLD=${OLD_APK_NAME}"
fi
- name: Detect target device(s)
run: |
set -euo pipefail
DEVICE_LINES="$(adb devices | awk 'NR>1 && $2=="device"{print $1}')"
if [[ -z "${DEVICE_LINES}" ]]; then
echo "ERROR: No online Android devices found."
exit 1
fi
TARGET="${{ inputs.androidDeviceId }}"
if [[ -n "$TARGET" ]]; then
if ! printf '%s\n' "$DEVICE_LINES" | grep -qx "$TARGET"; then
echo "ERROR: androidDeviceId '$TARGET' not found in adb devices."
exit 1
fi
DEVICE_LIST="$TARGET"
else
DEVICE_LIST="$(printf '%s\n' "$DEVICE_LINES" | xargs)"
fi
DEVICE_COUNT="$(wc -w <<<"${DEVICE_LIST}" | tr -d ' ')"
echo "DEVICE_LIST=${DEVICE_LIST}" >> "$GITHUB_ENV"
echo "DEVICE_COUNT=${DEVICE_COUNT}" >> "$GITHUB_ENV"
echo "Using device(s): ${DEVICE_LIST}"
- name: Install APK(s) on device(s)
run: |
set -euo pipefail
: "${DEVICE_LIST:?DEVICE_LIST missing}"
: "${APP_ID:?APP_ID missing}"
: "${NEW_APK_PATH:?NEW_APK_PATH missing}"
INSTALL_FLAGS="-r"
if [[ "${{ inputs.enforceAppInstall }}" == "true" ]]; then
INSTALL_FLAGS="-r -d"
fi
PACKAGES=(
"com.wire"
"com.waz.zclient.dev"
"com.wire.internal"
"com.wire.android.internal"
"com.wire.android.bund"
"com.wire.android.bund.column2"
"com.wire.android.bund.column3"
"com.waz.zclient.dev.debug"
)
read -ra DEVICES <<< "${DEVICE_LIST}"
for SERIAL in "${DEVICES[@]}"; do
ADB="adb -s ${SERIAL}"
${ADB} wait-for-device
# Clean baseline: uninstall known package ids if present
INSTALLED="$(${ADB} shell pm list packages || true)"
for pkg in "${PACKAGES[@]}"; do
if echo "${INSTALLED}" | grep -qx "package:${pkg}"; then
${ADB} uninstall "${pkg}" || true
fi
done
if [[ "${{ inputs.isUpgrade }}" == "true" ]]; then
: "${OLD_APK_PATH:?OLD_APK_PATH missing for upgrade}"
${ADB} install ${INSTALL_FLAGS} "${OLD_APK_PATH}"
fi
${ADB} install ${INSTALL_FLAGS} "${NEW_APK_PATH}"
if ! ${ADB} shell pm list packages | grep -qx "package:${APP_ID}"; then
echo "ERROR: '${APP_ID}' not installed on ${SERIAL}."
exit 1
fi
done
- name: Install 1Password CLI
uses: 1password/install-cli-action@v2
- name: Fetch secrets.json (runtime only)
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
run: |
set -euo pipefail
if [[ -z "${OP_SERVICE_ACCOUNT_TOKEN:-}" ]]; then
echo "ERROR: Missing OP_SERVICE_ACCOUNT_TOKEN secret"
exit 1
fi
chmod +x ./gradlew
SECRETS_FILE="${RUNNER_TEMP}/secrets.json"
export SECRETS_JSON_PATH="${SECRETS_FILE}"
echo "SECRETS_JSON_PATH=${SECRETS_FILE}" >> "$GITHUB_ENV"
./gradlew :tests:testsSupport:fetchSecrets \
--no-daemon \
--no-configuration-cache \
-PsecretsJsonPath="${SECRETS_FILE}" \
-PopVault="${OP_VAULT}"
test -s "${SECRETS_FILE}"
chmod 600 "${SECRETS_FILE}"
echo "OK: secrets.json created at ${SECRETS_FILE}"
- name: Build test APK (assemble)
run: |
set -euo pipefail
./gradlew :tests:testsCore:assembleDebugAndroidTest --no-daemon --no-configuration-cache
- name: Run UI tests (one shard per device)
env:
RESOLVED_TESTCASE_ID: ${{ needs.validate-and-resolve-inputs.outputs.resolvedTestCaseId }}
RESOLVED_CATEGORY: ${{ needs.validate-and-resolve-inputs.outputs.resolvedCategory }}
run: |
set -euo pipefail
: "${DEVICE_LIST:?DEVICE_LIST missing}"
: "${DEVICE_COUNT:?DEVICE_COUNT missing}"
GRADLE_ARGS=(
":tests:testsCore:connectedDebugAndroidTest"
"--no-daemon"
"--no-configuration-cache"
)
if [[ -n "${RESOLVED_TESTCASE_ID}" ]]; then
GRADLE_ARGS+=("-Pandroid.testInstrumentationRunnerArguments.testCaseId=${RESOLVED_TESTCASE_ID}")
fi
if [[ -n "${RESOLVED_CATEGORY}" ]]; then
GRADLE_ARGS+=("-Pandroid.testInstrumentationRunnerArguments.category=${RESOLVED_CATEGORY}")
fi
NUM_SHARDS="${DEVICE_COUNT}"
if [[ -n "${RESOLVED_TESTCASE_ID}" ]]; then
NUM_SHARDS="1"
fi
read -ra DEVICES <<< "${DEVICE_LIST}"
echo "Sharding: numShards=${NUM_SHARDS}, devices=${DEVICE_LIST}"
pids=()
shard_index=0
for SERIAL in "${DEVICES[@]}"; do
(
set -euo pipefail
export ANDROID_SERIAL="${SERIAL}"
export GRADLE_USER_HOME="${RUNNER_TEMP}/gradle-${SERIAL}"
./gradlew "${GRADLE_ARGS[@]}" \
"-Pandroid.testInstrumentationRunnerArguments.numShards=${NUM_SHARDS}" \
"-Pandroid.testInstrumentationRunnerArguments.shardIndex=${shard_index}"
) &
pids+=("$!")
shard_index=$((shard_index + 1))
done
failed=0
for pid in "${pids[@]}"; do
if ! wait "$pid"; then
failed=1
fi
done
if [[ "$failed" -ne 0 ]]; then
echo "ERROR: One or more shards failed."
exit 1
fi
- name: Pull Allure results from device(s)
if: always()
run: |
set -euo pipefail
if [[ -z "${DEVICE_LIST:-}" ]]; then
echo "No devices detected (skipping allure pull)"
exit 0
fi
OUT_DIR="${RUNNER_TEMP}/allure-results"
mkdir -p "${OUT_DIR}"
read -ra DEVICES <<< "${DEVICE_LIST}"
for SERIAL in "${DEVICES[@]}"; do
echo "Pulling allure-results from ${SERIAL}..."
mkdir -p "${OUT_DIR}/${SERIAL}"
adb -s "${SERIAL}" pull "/sdcard/googletest/test_outputfiles/allure-results" "${OUT_DIR}/${SERIAL}" >/dev/null 2>&1 || true
done
- name: Merge Allure results (add device label)
if: always()
env:
OUT_DIR: ${{ runner.temp }}/allure-results
MERGED_DIR: ${{ runner.temp }}/allure-results-merged
run: |
set -euo pipefail
python3 - <<'PY'
import json
import os
import shutil
import subprocess
from pathlib import Path
out_dir = Path(os.environ["OUT_DIR"])
merged_dir = Path(os.environ["MERGED_DIR"])
merged_dir.mkdir(parents=True, exist_ok=True)
def get_prop(serial: str, prop: str) -> str:
try:
result = subprocess.run(
["adb", "-s", serial, "shell", "getprop", prop],
check=False,
capture_output=True,
text=True,
timeout=5,
)
return result.stdout.strip()
except Exception:
return ""
device_dirs = [p for p in out_dir.iterdir() if p.is_dir()]
device_info = {}
for device_dir in device_dirs:
serial = device_dir.name
model = get_prop(serial, "ro.product.model") or "unknown"
sdk = get_prop(serial, "ro.build.version.release") or get_prop(serial, "ro.build.version.sdk") or "unknown"
device_info[serial] = {"model": model, "sdk": sdk}
def device_label(serial: str) -> str:
meta = device_info.get(serial, {})
model = meta.get("model") or "unknown"
sdk = meta.get("sdk") or "unknown"
return f"{model} - {sdk} ({serial})"
def add_label(data: dict, name: str, value: str) -> dict:
labels = [l for l in data.get("labels", []) if l.get("name") != name]
labels.append({"name": name, "value": value})
data["labels"] = labels
return data
for device_dir in device_dirs:
serial = device_dir.name
src_dir = device_dir / "allure-results"
if not src_dir.is_dir():
src_dir = device_dir
if not src_dir.is_dir():
continue
label = device_label(serial)
for item in src_dir.iterdir():
if item.is_dir():
continue
if item.name in ("executor.json", "environment.properties"):
continue
if item.name.endswith("-result.json"):
try:
data = json.loads(item.read_text(encoding="utf-8"))
except Exception:
continue
data = add_label(data, "device", label)
(merged_dir / item.name).write_text(
json.dumps(data, ensure_ascii=True),
encoding="utf-8",
)
else:
shutil.copy2(item, merged_dir / item.name)
if device_info:
devices = ", ".join(device_label(serial) for serial in sorted(device_info.keys()))
(merged_dir / "environment.properties").write_text(
f"devices={devices}\n", encoding="utf-8"
)
run_id = os.environ.get("GITHUB_RUN_ID", "")
repo = os.environ.get("GITHUB_REPOSITORY", "")
server = os.environ.get("GITHUB_SERVER_URL", "https://github.com")
run_url = f"{server}/{repo}/actions/runs/{run_id}" if repo and run_id else ""
executor = {
"name": "GitHub Actions",
"type": "github",
"url": run_url,
"buildName": os.environ.get("GITHUB_RUN_NUMBER", ""),
"buildUrl": run_url,
"reportName": "Android UI Tests",
}
(merged_dir / "executor.json").write_text(
json.dumps(executor, ensure_ascii=True),
encoding="utf-8",
)
PY
- name: Generate Allure HTML report
if: always()
env:
MERGED_DIR: ${{ runner.temp }}/allure-results-merged
REPORT_DIR: ${{ runner.temp }}/allure-report
run: |
set -euo pipefail
if [[ ! -d "${MERGED_DIR}" ]]; then
echo "No merged Allure results found"
exit 0
fi
ALLURE_VERSION="2.29.0"
ALLURE_TGZ="${RUNNER_TEMP}/allure-${ALLURE_VERSION}.tgz"
curl -fsSL -o "${ALLURE_TGZ}" \
"https://github.com/allure-framework/allure2/releases/download/${ALLURE_VERSION}/allure-${ALLURE_VERSION}.tgz"
tar -xzf "${ALLURE_TGZ}" -C "${RUNNER_TEMP}"
"${RUNNER_TEMP}/allure-${ALLURE_VERSION}/bin/allure" \
generate "${MERGED_DIR}" -o "${REPORT_DIR}" --clean
- name: Upload test artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: android-ui-test-results
if-no-files-found: warn
retention-days: 90
path: |
tests/testsCore/build/reports/androidTests/connected
${{ runner.temp }}/allure-results-merged
${{ runner.temp }}/allure-report
- name: Cleanup (remove secrets + build outputs)
if: always()
run: |
set -euo pipefail
rm -f "${RUNNER_TEMP}/secrets.json" || true
rm -f "${RUNNER_TEMP}/Wire.apk" "${RUNNER_TEMP}/Wire.old.apk" || true
rm -rf "${RUNNER_TEMP}/allure-results" || true
rm -rf "${RUNNER_TEMP}/allure-results-merged" || true
rm -rf "${RUNNER_TEMP}/allure-report" || true
# Remove build outputs (these can contain BuildConfig secrets) but keep Gradle caches
git clean -ffdx -e .gradle -e .kotlin