Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 30 additions & 59 deletions .github/workflows/migration-wallet-setup.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,25 @@ on:
workflow_call:
inputs:
e2e_branch:
description: "Candidate branch of synonymdev/bitkit-e2e-tests. Re-validated immediately before checkout. Callers may pass an already-resolved ref; a missing default companion falls back to main."
description: 'Candidate branch of synonymdev/bitkit-e2e-tests. Re-validated immediately before checkout. Callers may pass an already-resolved ref; a missing default companion falls back to main.'
required: true
type: string
e2e_branch_input:
description: "Resolution mode (main | default-feature-branch | custom branch name). Empty inherits the caller workflow_dispatch input e2e_branch, then default-feature-branch. A custom name hard-fails when the branch is missing."
description: 'Resolution mode (main | default-feature-branch | custom branch name). Empty inherits the caller workflow_dispatch input e2e_branch, then default-feature-branch. A custom name hard-fails when the branch is missing.'
required: false
type: string
default: ""
default: ''
rn_version:
description: "Legacy RN app version to use for setup (e.g., v1.1.6)"
description: 'Legacy RN app version to use for setup (e.g., v1.1.6)'
required: false
type: string
default: "v1.1.6"
default: 'v1.1.6'
setup_type:
description: "Wallet setup type (standard | passphrase | sweep)"
description: 'Wallet setup type (standard | passphrase | sweep)'
required: true
type: string
scenario_name:
description: "Migration scenario name for artifact naming"
description: 'Migration scenario name for artifact naming'
required: true
type: string

Expand Down Expand Up @@ -59,12 +59,13 @@ jobs:
sudo udevadm trigger --name-match=kvm

- name: Download RN app for migration
working-directory: bitkit-e2e-tests
env:
RN_VERSION: ${{ inputs.rn_version }}
run: |
mkdir -p bitkit-e2e-tests/aut
curl -L -o bitkit-e2e-tests/aut/bitkit_rn_regtest.apk \
https://github.com/synonymdev/bitkit-e2e-tests/releases/download/migration-rn-regtest/bitkit_rn_regtest_${{ inputs.rn_version }}.apk
# Symlink to bitkit_e2e.apk so wdio.conf.ts can initialize the Appium session
cd bitkit-e2e-tests/aut && ln -sf bitkit_rn_regtest.apk bitkit_e2e.apk
python3 scripts/download-migration-app.py android rn "$RN_VERSION"
# WDIO initializes its session using the RN source during preparation.
ln -sf bitkit_rn_regtest.apk aut/bitkit_e2e.apk

- name: Setup Node.js
uses: actions/setup-node@v4
Expand All @@ -83,32 +84,7 @@ jobs:
working-directory: bitkit-e2e-tests
run: npm ci

- name: Clear previous migration env files
working-directory: bitkit-e2e-tests
run: |
rm -f artifacts/migration_setup_standard.env
rm -f artifacts/migration_setup_passphrase.env
rm -f artifacts/migration_setup_sweep.env

- name: Prepare migration wallet 1
continue-on-error: true
id: prepare1
uses: reactivecircus/android-emulator-runner@v2
with:
profile: pixel_6
api-level: 33
arch: x86_64
avd-name: Pixel_6
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim -camera-front none
script: cd bitkit-e2e-tests && ./ci_run_android.sh --mochaOpts.grep "@migration_setup_${{ inputs.setup_type }}"
env:
BACKEND: regtest

- name: Prepare migration wallet 2
continue-on-error: true
id: prepare2
if: steps.prepare1.outcome != 'success'
- name: Prepare independent wallets for all consumer attempts
uses: reactivecircus/android-emulator-runner@v2
with:
profile: pixel_6
Expand All @@ -117,32 +93,27 @@ jobs:
avd-name: Pixel_6
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim -camera-front none
script: cd bitkit-e2e-tests && ./ci_run_android.sh --mochaOpts.grep "@migration_setup_${{ inputs.setup_type }}"
script: cd bitkit-e2e-tests && bash scripts/prepare-migration-wallets.sh "$SETUP_TYPE"
env:
BACKEND: regtest
SETUP_TYPE: ${{ inputs.setup_type }}

- name: Prepare migration wallet 3
id: prepare3
if: steps.prepare1.outcome != 'success' && steps.prepare2.outcome != 'success'
uses: reactivecircus/android-emulator-runner@v2
- name: Upload preparation diagnostics
if: always()
uses: actions/upload-artifact@v4
with:
profile: pixel_6
api-level: 33
arch: x86_64
avd-name: Pixel_6
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim -camera-front none
script: cd bitkit-e2e-tests && ./ci_run_android.sh --mochaOpts.grep "@migration_setup_${{ inputs.setup_type }}"
env:
BACKEND: regtest

- name: Verify migration env file
run: |
set -euo pipefail
test -f "bitkit-e2e-tests/artifacts/migration_setup_${{ inputs.setup_type }}.env"
name: migration-setup-logs_${{ inputs.scenario_name }}_${{ github.run_attempt }}
path: |
bitkit-e2e-tests/artifacts/attempt-*/
bitkit-e2e-tests/artifacts/migration-source.json

- name: Upload migration env file
uses: actions/upload-artifact@v4
with:
name: migration-env_${{ inputs.rn_version }}_${{ inputs.scenario_name }}
path: bitkit-e2e-tests/artifacts/migration_setup_${{ inputs.setup_type }}.env
name: migration-env_${{ inputs.rn_version }}_${{ inputs.scenario_name }}_${{ github.run_attempt }}
path: |
bitkit-e2e-tests/artifacts/wallet-attempt-1/
bitkit-e2e-tests/artifacts/wallet-attempt-2/
bitkit-e2e-tests/artifacts/wallet-attempt-3/
if-no-files-found: error
retention-days: 1
4 changes: 4 additions & 0 deletions config/migration-baselines.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"android": "2.5.0",
"ios": "2.5.0"
}
6 changes: 6 additions & 0 deletions config/migration-checksums.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"2.5.0": {
"bitkit_e2e.apk": "03834e6ff2fe7026438a2fb2a3eb59289062eea504964c3a7d152757f2e7f462",
"Bitkit.app.zip": "e7e26bcf77bf4dea089f321ffd1a4e0932672e03c20ea9b825de6f93c06088ae"
}
}
81 changes: 81 additions & 0 deletions docs/migration-tests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Migration tests

The routine migration suite has four cases per platform. Every case starts with savings and spending:

| Source | Restore into a clean installation | Install current app on top |
| ----------------------- | --------------------------------- | --------------------------- |
| React Native 1.1.6 | `@migration_rn_restore` | `@migration_rn_upgrade` |
| Previous native release | `@migration_native_restore` | `@migration_native_upgrade` |

All apps and tests use remote **regtest**. The native baseline is pinned per platform in `config/migration-baselines.json` (currently 2.5.0), not selected from GitHub's latest release. The app workflows retain nightly, manual, and release-PR triggers. Manual `previous_native_version` overrides the pinned tag. Routine runs do not prepare or run passphrase/legacy-address wallets.

## Run locally

Use a dedicated emulator or simulator, with source and target apps built for regtest. Keep the current target in `aut/bitkit_e2e.apk` or `aut/Bitkit.app`.

```sh
python3 scripts/migration-matrix.py android
python3 scripts/download-migration-app.py android native 2.5.0
BACKEND=regtest PREVIOUS_NATIVE_VERSION=2.5.0 \
PREVIOUS_NATIVE_APP_PATH="$PWD/aut/previous-native/bitkit_e2e.apk" \
ANDROID_UDID="$MIGRATION_ANDROID_SERIAL" ANDROID_SERIAL="$MIGRATION_ANDROID_SERIAL" \
APPIUM_PORT=4725 ANDROID_SYSTEM_PORT=8205 \
npm run e2e:android -- --mochaOpts.grep '@migration_native_restore'
```

Set `MIGRATION_ANDROID_SERIAL` to the newly created emulator's serial. WDIO starts Appium on the selected `APPIUM_PORT`; choose free ports distinct from other active test runs. Both Android variables are required: the WDIO session uses `ANDROID_UDID`, while shell-based helpers use `ANDROID_SERIAL`.

For iOS, download with `ios native 2.5.0`, then run directly against the newly created simulator:

```sh
BACKEND=regtest PREVIOUS_NATIVE_VERSION=2.5.0 \
PREVIOUS_NATIVE_APP_PATH="$PWD/aut/previous-native/Bitkit.app" \
SIMULATOR_UDID="$MIGRATION_SIMULATOR_UDID" SIMULATOR_NAME="$MIGRATION_SIMULATOR_NAME" \
SIMULATOR_OS_VERSION="$MIGRATION_IOS_VERSION" APPIUM_PORT=4727 IOS_WDA_LOCAL_PORT=8105 \
npm run e2e:ios -- --mochaOpts.grep '@migration_native_restore'
```

Set the three `MIGRATION_*` simulator variables to that dedicated device's values. Prefer direct npm commands for isolated local runs: `ci_run_ios.sh` resolves a device by name, and `ci_run_android.sh` calls adb, so a WDIO UDID alone does not isolate those wrappers. Use `@migration_native_upgrade` for install-on-top. Native iOS creates wallets directly on iOS and never depends on Android preparation.

Download RN sources with `python3 scripts/download-migration-app.py android rn v1.1.6` (or `ios rn v1.1.6`). Android RN cases prepare their own wallets. RN iOS still needs wallets created on Android because RN iOS cannot reliably be driven through the creation flow with Appium. The reusable `migration-wallet-setup.yml` handles that in CI. The RN iOS restore case skips downloading the unused RN iOS app; its Android source provenance remains in the matching `migration-setup-logs` artifact. It verifies balances and activity but does not assert Android tag metadata because cross-platform mnemonic restore does not currently recover those tags. RN iOS install-on-top and both Android RN cases continue to verify tags.

For RN iOS local runs, prepare each wallet on the dedicated Android emulator with `BACKEND=regtest MIGRATION_SETUP_WALLET=1 ANDROID_UDID="$MIGRATION_ANDROID_SERIAL" ANDROID_SERIAL="$MIGRATION_ANDROID_SERIAL" APPIUM_PORT=4725 ANDROID_SYSTEM_PORT=8205 npm run e2e:android -- --mochaOpts.grep '@migration_setup_standard'`. Load the resulting `artifacts/migration_setup_standard.env` into the environment before running the chosen RN iOS case. Treat these files as secrets and never print the seed in diagnostics. Use a fresh setup wallet for each case and retry.

Native verification checks separate savings and spending balances, recovered activity and tags, state after relaunch, and receipt of a small Lightning payment. The payment demonstrates incoming Lightning usability; it does not claim outbound payment coverage.

Source preparation verifies backup completion after funding and metadata changes. Native builds expose `AllSynced`; RN release builds require successful `Latest Backup` statuses for every expected category, including Tags and Connections when spending exists. RN checks these before exporting a wallet or installing the target.

## Retries and extended coverage

Native and Android RN cases create new wallets inside every test attempt. iOS RN has two Android preparation jobs in routine runs, one for restore and one for upgrade. Each prepares **three independent wallets**, one per possible consumer attempt, with up to three setup attempts per wallet. This costs extra setup time but prevents a partial migration from modifying the backup used by its retry. Setup completion markers include the wallet number.

Use **Re-run all jobs** when rerunning a failed iOS RN workflow. Wallet artifact names include the workflow run attempt: rerunning only consumer jobs intentionally fails to download old wallets, instead of reusing potentially modified backups. Each attempt loads only its own wallet environment. Wallet artifacts have one-day retention.

Set manual dispatch input `extended_rn=true` to add RN 1.1.6 passphrase (`@migration_3`) and legacy-address (`@migration_4`) cases. Locally set `MIGRATION_EXTENDED=true` and select the corresponding tag. These are retained targeted coverage, excluded from the four routine cases.

The first two test failures allow retries; the third failure fails its job. The `migration-result` job requires every expected job to succeed, including setup and plan resolution. Skipped required jobs count as failure. Slack's overall migration result uses this gate. Source metadata and test diagnostics are uploaded even on success to preserve retry evidence.

## Advance the baseline

1. Archive the released regtest builds using `/archive-release` after validating that release.
2. Verify the archived APK and simulator zip install on CI devices, including Android package/signing compatibility and iOS simulator architecture/runtime compatibility.
3. Update the relevant platform in `config/migration-baselines.json` to its exact release tag. Do not advance automatically when an archive is uploaded.
4. Record the release asset SHA-256 digests in `config/migration-checksums.json`; the downloader verifies known checksums before installing/extracting. Overrides without a recorded checksum still log the calculated checksum.
5. Run the four cases on that platform and review failures before merging the baseline update.

The downloader fails on missing/empty/malformed assets or checksum mismatches. Previous native apps live under `aut/previous-native/`, so the target cannot be overwritten. `artifacts/migration-source.json` records source URL/tag/checksum, target revision, and E2E revision. It contains no wallet seed.

## Coordinated rollout and known failures

Land the E2E changes (including the reusable setup workflow) before enabling the new app workflows. GitHub reusable workflow references use `@main`; companion branches select the E2E scripts/tests but do not change which reusable workflow definition GitHub executes. Before rollout, validation of that definition requires an explicit temporary reference to the companion branch. Keep matching companion branch names across repositories for test-code resolution.

Android restore may expose [bitkit-android #1342](https://github.com/synonymdev/bitkit-android/issues/1342). Preserve failing restore assertions and report the issue separately from infrastructure failures; do not skip the case or accept missing funds to get a passing run.

Static helper checks:

```sh
python3 -m unittest discover -s scripts -p 'test_migration_tools.py'
bash -n scripts/prepare-migration-wallets.sh
```

Device passes and hosted workflow passes are separate evidence. Local runs cannot prove GitHub artifact transfer or job dependency execution.
1 change: 1 addition & 0 deletions scripts/build-android-apk.sh
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ echo "Building Android APK (BACKEND=$BACKEND, E2E_BACKEND=$E2E_BACKEND, TREZOR_B
pushd "$ANDROID_ROOT" >/dev/null
E2E=true \
E2E_BACKEND="$E2E_BACKEND" \
GEO=false \
TREZOR_BRIDGE="$TREZOR_BRIDGE" \
TREZOR_BRIDGE_URL="$TREZOR_BRIDGE_URL" \
./gradlew "$GRADLE_TASK" --no-daemon --stacktrace
Expand Down
4 changes: 3 additions & 1 deletion scripts/build-ios-sim.sh
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ XCODE_EXTRA_ARGS+=(
"E2E_NETWORK=$E2E_NETWORK"
"TREZOR_BRIDGE=$TREZOR_BRIDGE"
"TREZOR_BRIDGE_URL=$TREZOR_BRIDGE_URL"
"SWIFT_ACTIVE_COMPILATION_CONDITIONS=\$(inherited) E2E_BUILD"
# Archived E2E builds run on hosted US runners. Do not inherit the Debug
# configuration's CHECK_GEOBLOCK flag or spending setup is unavailable.
"SWIFT_ACTIVE_COMPILATION_CONDITIONS=DEBUG E2E_BUILD"
)

if [[ -n "$TREZOR_ELECTRUM_URL" ]]; then
Expand Down
93 changes: 93 additions & 0 deletions scripts/download-migration-app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#!/usr/bin/env python3
"""Download a migration source, keeping previous native builds separate from targets."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import re
import shutil
import subprocess
import tempfile
import zipfile

ROOT = Path(__file__).resolve().parents[1]
RELEASES = "https://github.com/synonymdev/bitkit-e2e-tests/releases/download"


def download(platform, source, version):
if source == "rn":
if version != "v1.1.6":
raise ValueError("Only React Native v1.1.6 is supported")
elif not re.fullmatch(r"\d+\.\d+\.\d+(?:[-.][A-Za-z0-9]+)*", version):
raise ValueError(f"Invalid native release tag: {version!r}")
if source == "native":
tag = version
asset = {"android": "bitkit_e2e.apk", "ios": "Bitkit.app.zip"}[platform]
destination = ROOT / "aut" / "previous-native"
else:
tag = "migration-rn-regtest"
asset = {
"android": f"bitkit_rn_regtest_{version}.apk",
"ios": f"bitkit_rn_regtest_ios_{version}.zip",
}[platform]
destination = ROOT / "aut"
destination.mkdir(parents=True, exist_ok=True)
url = f"{RELEASES}/{tag}/{asset}"
with tempfile.TemporaryDirectory(dir=destination) as staging:
archive = Path(staging) / asset
subprocess.run(["curl", "--fail", "--location", "--retry", "3", "--connect-timeout", "30", "--max-time", "900", "--output", str(archive), url], check=True)
if not archive.stat().st_size:
raise ValueError(f"Empty migration asset: {url}")
checksum = hashlib.sha256()
with archive.open("rb") as data:
for chunk in iter(lambda: data.read(1024 * 1024), b""):
checksum.update(chunk)
digest = checksum.hexdigest()
checksums = json.loads((ROOT / "config/migration-checksums.json").read_text())
expected = checksums.get(tag, {}).get(asset)
if expected and digest != expected:
raise ValueError(f"Checksum mismatch for {url}: expected {expected}, received {digest}")
if platform == "android":
with zipfile.ZipFile(archive) as apk:
if "AndroidManifest.xml" not in apk.namelist():
raise ValueError(f"Not an Android APK: {url}")
app = destination / ("bitkit_e2e.apk" if source == "native" else "bitkit_rn_regtest.apk")
shutil.move(str(archive), app)
else:
bundle = "Bitkit.app" if source == "native" else f"bitkit_rn_regtest_ios_{version}.app"
with zipfile.ZipFile(archive) as zipped:
for entry in zipped.namelist():
if Path(entry).is_absolute() or ".." in Path(entry).parts:
raise ValueError(f"Unsafe archive entry: {entry}")
if f"{bundle}/Info.plist" not in zipped.namelist():
raise ValueError(f"Archive does not contain {bundle}/Info.plist")
# ditto preserves executable permissions and bundle symlinks on macOS.
subprocess.run(["ditto", "-x", "-k", str(archive), staging], check=True)
app = destination / ("Bitkit.app" if source == "native" else "bitkit_rn_regtest_ios.app")
if app.exists():
shutil.rmtree(app)
shutil.move(str(Path(staging) / bundle), app)
metadata = {"platform": platform, "source": source, "version": version, "url": url, "sha256": digest,
"app_path": str(app), "target_revision": os.getenv("GITHUB_SHA", "local"),
"e2e_revision": subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=ROOT, text=True).strip()}
artifacts = ROOT / "artifacts"
artifacts.mkdir(exist_ok=True)
(artifacts / "migration-source.json").write_text(json.dumps(metadata, indent=2) + "\n")
print(json.dumps(metadata, indent=2))
if source == "native" and os.getenv("GITHUB_ENV"):
with open(os.environ["GITHUB_ENV"], "a") as env:
env.write(f"PREVIOUS_NATIVE_APP_PATH={app}\nPREVIOUS_NATIVE_VERSION={version}\n")
return app


if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("platform", choices=("android", "ios"))
parser.add_argument("source", choices=("rn", "native"))
parser.add_argument("version")
args = parser.parse_args()
try:
download(args.platform, args.source, args.version)
except (ValueError, OSError, subprocess.CalledProcessError, zipfile.BadZipFile) as error:
parser.exit(1, f"Migration source download failed: {error}\n")
Loading