Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
e8d3ef6
test: add explicit E2E inventory and run-bound reporting
piotr-iohk Nov 3, 2026
5f3f28b
test: add focused regtest smoke and independent payment evidence
piotr-iohk Nov 3, 2026
9d08c2f
test: fix smoke precision, TLS and execution identity boundaries
piotr-iohk Nov 3, 2026
b49b22b
docs: validate smoke manually before automatic gate cutover
piotr-iohk Nov 3, 2026
49d99b3
ci: select iOS risk coverage and retain retry evidence
piotr-iohk Nov 3, 2026
a4996b5
Serialize planned specs and pin migration fixture source
piotr-iohk Nov 3, 2026
39f8964
Use supported cache action for migration fixtures
piotr-iohk Nov 3, 2026
ee9e8be
Move app path selection out of shared E2E repository
piotr-iohk Nov 3, 2026
6868714
test: keep legacy sweep fixture on wallet home
piotr-iohk Nov 3, 2026
d2aea6b
ci: seal bounded execution budgets into test plans
piotr-iohk Nov 3, 2026
fce4fa1
docs: record full staging execution budget
piotr-iohk Nov 3, 2026
e49cfab
ci: validate RN migration inputs and bound preparation logs
piotr-iohk Nov 4, 2026
6f4492f
ci: allow full local packs to finish and handle early QuickPay
piotr-iohk Nov 4, 2026
f91c7f4
test: handle QuickPay throughout transfer verification
piotr-iohk Nov 4, 2026
ca9e731
docs: distinguish PR E2E from nightly and release migrations
piotr-iohk Nov 4, 2026
ee7a429
test: verify the accessible legacy iOS wallet balance
piotr-iohk Nov 4, 2026
b094f31
docs: describe independent iOS migration runs
piotr-iohk Nov 4, 2026
b691df2
ci: restore visible failed-only iOS retries
piotr-iohk Nov 4, 2026
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
46 changes: 46 additions & 0 deletions .github/workflows/inventory.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: E2E inventory and reporting

on:
pull_request:
paths:
- 'test/**'
- 'scripts/e2e-inventory.ts'
- 'scripts/ci-*'
- 'scripts/verify-e2e-sha.sh'
- '.github/workflows/migration-wallet-setup.yml'
- 'wdio.conf.ts'
- 'package*.json'
- '.github/workflows/inventory.yml'
push:
branches: [main]
paths:
- 'test/**'
- 'scripts/e2e-inventory.ts'
- 'scripts/ci-*'
- 'scripts/verify-e2e-sha.sh'
- '.github/workflows/migration-wallet-setup.yml'
- 'wdio.conf.ts'
- 'package*.json'
- '.github/workflows/inventory.yml'
workflow_dispatch:

permissions:
contents: read

concurrency:
group: inventory-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
inventory:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: '22'
cache: npm
- run: npm ci --ignore-scripts
- run: npm run inventory:check
- run: npm run test:reporting
56 changes: 31 additions & 25 deletions .github/workflows/migration-wallet-setup.yml
Original file line number Diff line number Diff line change
@@ -1,63 +1,73 @@
name: Migration Wallet Setup

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

e2e_sha:
description: Optional immutable E2E commit. Checkout must match; never resolve or fall back as a branch.
type: string
default: ""
diagnostics_retention_days:
description: Preparation-log retention in days; zero preserves the repository default.
type: number
default: 0
env:
TERM: xterm-256color
FORCE_COLOR: 1

jobs:
prepare-wallets:
runs-on: ubuntu-latest

steps:
# Re-resolve in this job. "Re-run failed jobs" does not re-run a
# successful resolve job in the caller, so inputs.e2e_branch can name
# a companion that was deleted after that job finished.
- name: Validate pinned commit
if: inputs.e2e_sha != ''
env:
E2E_SHA: ${{ inputs.e2e_sha }}
run: "[[ \"$E2E_SHA\" =~ ^[0-9a-f]{40}$ ]] || { echo 'Invalid pinned E2E commit' >&2; exit 1; }"
- name: Resolve E2E tests ref
id: e2e-ref
uses: synonymdev/bitkit-e2e-tests/.github/actions/resolve-e2e-ref@main
with:
selected: ${{ inputs.e2e_branch }}
e2e_branch_input: ${{ inputs.e2e_branch_input || github.event.inputs.e2e_branch || 'default-feature-branch' }}

if: inputs.e2e_sha == ''
- name: Clone E2E tests
uses: actions/checkout@v4
with:
repository: synonymdev/bitkit-e2e-tests
path: bitkit-e2e-tests
ref: ${{ steps.e2e-ref.outputs.ref }}

ref: ${{ inputs.e2e_sha || steps.e2e-ref.outputs.ref }}
- name: Verify pinned checkout
if: inputs.e2e_sha != ''
working-directory: bitkit-e2e-tests
env:
E2E_SHA: ${{ inputs.e2e_sha }}
run: bash scripts/verify-e2e-sha.sh checkout
- 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: Download RN app for migration
working-directory: bitkit-e2e-tests
env:
Expand All @@ -66,24 +76,20 @@ jobs:
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
with:
node-version: 22

- name: Cache npm cache
uses: actions/cache@v3
uses: actions/cache@v6
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-

- name: Install dependencies
working-directory: bitkit-e2e-tests
run: npm ci

- name: Prepare independent wallets for all consumer attempts
uses: reactivecircus/android-emulator-runner@v2
with:
Expand All @@ -97,16 +103,15 @@ jobs:
env:
BACKEND: regtest
SETUP_TYPE: ${{ inputs.setup_type }}

- name: Upload preparation diagnostics
if: always()
uses: actions/upload-artifact@v4
with:
name: migration-setup-logs_${{ inputs.scenario_name }}_${{ github.run_attempt }}
retention-days: ${{ inputs.diagnostics_retention_days }}
path: |
bitkit-e2e-tests/artifacts/attempt-*/
bitkit-e2e-tests/artifacts/migration-source.json

- name: Upload migration env file
uses: actions/upload-artifact@v4
with:
Expand All @@ -118,3 +123,4 @@ jobs:
bitkit-e2e-tests/artifacts/wallet-attempt-3/
if-no-files-found: error
retention-days: 1
timeout-minutes: 90
24 changes: 4 additions & 20 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,26 +169,10 @@ Implication for feature work:
- Keep Android/iOS platform differences behind helpers in `test/helpers/`.
- Prefer extracting shared flows into `test/helpers/` over copying logic between specs. Keep helpers small and reuse existing ones before adding new test code.

## Local vs staging tags
## iOS CI selection

The app merge gate (`e2e.yml` / `e2e-tests.yml`) runs `BACKEND=local` (docker Electrum/LND). Staging (`e2e-staging.yml`) and migration (`e2e_migration.yml`) run `BACKEND=regtest` against stag0.
The iOS consumer owns its path policy in `bitkit-ios/.github/e2e-policy.json` and selector in `.github/scripts/e2e-selection.cjs`. It reads this repo's `test/inventory.json`; iOS paths and selection policy do not belong in this shared repo. Historical `@ios_gate` and `@ios_nightly` tags do not control that consumer. Its base cases are `send_1`, `send_2`, `lightning_1`, `transfer_2`, `boost_2`; automatic risk selection expands coverage. Nightly E2E runs all applicable local/staging cases; the independent migration workflow runs all six migrations nightly and on release branches. See [inventory and reporting](docs/inventory-reporting.md).

Capability tags (`@settings`, `@boost`, `@send`, …) stay on titles so Android local can keep grepping them. iOS queue tags are disjoint from `@staging` and from `*_staging` (mocha grep is a regex substring).
Keep capability tags for Android's existing selectors and manual execution. Android workflows and selections are unchanged. Do not rename tags merely to match the iOS policy.

| Tag | Android local | iOS Mini (`e2e-tests.yml`) | Staging (`e2e-staging.yml`) |
|---|---|---|---|
| `@ios_gate` | ignored | merge gate grep | no |
| `@ios_nightly` | ignored (still run via `@settings` etc.) | no | extra iOS shard (`ios_nightly_ui`), GitHub mac, not Mini |
| `@transfer_1`, `@transfer_max` | no | no | `@transfer_staging` (keep `@transfer_1` / `@transfer_max` / `@transfer`). `@transfer_1` is not in app `e2e-staging.yml` yet — add the transfer shard once it is 3× green. |
| `@multi_address_2` | no | no | `@multi_address_staging` only |
| `@pubky` / `@paykit` / `@pubky_profile` | no | no | `@pubky_staging` (keep capability tags for other greps) |
| `@boost`, `@multi_address_3` | yes | `@ios_gate` (needs exclusive miner) | no |
| `@send_2`, `@send_3`, `@lightning`, `@lnurl`, `@transfer_2`, `@multi_address_4` | yes | `@ios_gate` (local LND) | no |
| `@hardware_wallet` | no (staging only) | `@ios_gate` | Android staging greps `@hardware_wallet`; do **not** add `@hardware_wallet_staging`. iOS Mini greps `@ios_gate` on this describe. No Trezor on GitHub mac. |
| `@onboarding`, `@backup`, `@receive`, `@onchain_1`, `@onchain_2`, `@send_1`, `@multi_address_1`, `@settings`, `@numberpad`, `@widgets`, `@security` | yes | `@ios_nightly` | iOS nightly shard |
| `@onchain_3` | yes (`@onchain`) | no | no |
| `@migration_*` | migration workflow (nightly, dispatch, `release-*` PRs) | same | no |

Put `@ios_gate` / `@ios_nightly` on `describe` when the whole file is one iOS queue; on `ciIt` when siblings split (`@send_1` vs `@send_2`/`_3`, `@multi_address_1` vs `_3`/`_4`). Local-LND specs (`@send_3`, `@multi_address_4`) stay `@ios_gate` — they cannot run on the regtest nightly shard. Mini keeps boost/RBF, local-LND money path, and hardware wallet.

Poke staging with `gh workflow run e2e-staging.yml` on the app repo (does not queue the iOS Mini). Optional Slack post to `#bitkit-staging-nightly` via dispatch `post_to_slack`.
Inventory-enabled iOS execution uses `scripts/ci-run.ts attempt` from three visible workflow steps. `ciIt()` skips cases that passed earlier in the same run, so only failed or uncompleted cases repeat. Completion locks are scoped to the run directory and rebuilt from verified worker results before each attempt. Reports combine current-run passes and retain first-failure evidence. Android non-inventory retries retain their existing `/tmp/lock` behavior. Plans and receipts must identify the app, test source, artifact, backend and run. Never reuse a previous night's pass as current execution.
63 changes: 63 additions & 0 deletions docs/inventory-reporting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# iOS E2E selection and reporting

Issue: https://github.com/synonymdev/bitkit-e2e-tests/issues/264

The iOS `e2e-tests.yml` entry point owns PR selection, manual validation and the nightly schedule. Staging is reusable. The independent `e2e_migration.yml` workflow owns its schedule, release pushes, app build and result. There is no shadow gate. Android workflow triggers, test selection, backend assignments and legacy retry behavior remain unchanged.

## PR selection

Every iOS app-code PR starts with these existing cases, unchanged:

- `send_1`, `send_2`, `lightning_1`, `transfer_2`, `boost_2`.

The iOS repo owns `.github/e2e-policy.json`, its selector and policy tests. Its tested app checkout supplies the policy; this repository supplies only the case inventory and execution/reporting. Change iOS paths and their policy together in the same PR. See [iOS policy documentation](https://github.com/synonymdev/bitkit-ios/blob/master/Docs/e2e-ci.md).

Feature directories and explicit service/model/helper files add only their relevant existing cases. For example, widget services add widget cases, Paykit services add staging, and backup services add recovery coverage. Only named shared wallet/lifecycle dependencies and build/orchestration configuration select every PR-eligible local and staging case. There is no blanket Services/Models/Managers/Utilities rule.

Cheap iOS validation checks all tracked paths, stale policy entries and referenced case IDs. Unknown paths fail with an actionable policy-update error before paid builds. Old and new rename paths contribute coverage from the base and tested policies, so a refactor cannot silently drop coverage. Documentation/native-test/policy-only changes skip device jobs. Selection records cases, reasons, app/E2E/policy identity and explicit gaps. USDT-specific paths retain the Bitcoin base gate and report the lack of existing USDT E2E coverage; unrelated Bitcoin tests do not establish USDT correctness.

PRs stacked on another branch retain the existing automatic exclusion. Manual dispatch with `pr` checks the PR's current merge against its actual base/head, including stacked PRs. The workflow source is the branch selected for dispatch. `selection=auto` uses the PR diff; `base` runs the five cases; `full` runs all applicable local and staging cases. Migration runs nightly and after each push to an iOS `release-*` branch. Manual E2E dispatches use local and staging cases. Automatic PRs cannot narrow selection with a manual input.

## Nightly

Two separate iOS workflows run each night at 03:00 UTC:

| Lane | Active existing cases |
| --- | --- |
| Local | 42: all core UI, local money, on-chain, multi-address and active hardware cases |
| Staging | 6: `pubky_profile_1` through `pubky_profile_5`, `paykit_1` |
| Independent migration workflow | 6: native restore/upgrade, RN restore/upgrade, passphrase and legacy-address migration |

The 54 cases use the appropriate local or remote-regtest build and fixtures. Migration setup entries are fixtures, not extra regression passes. Five existing skipped cases and six unsupported iOS mainnet cases remain explicit gaps; the interactive Lightning payment utility remains manual. This does not invent missing USDT, allowance or subscription automation.

## Attempts and evidence

`scripts/ci-run.ts` prepares a plan tied to the actual E2E checkout, selected app SHA, downloaded artifact digest, platform, backend, variant and run identity. Selection rejects unknown, skipped or wrong-backend cases. Only selected spec files load.

Local and staging jobs expose `Run E2E Tests 1`, `2`, and `3` as separate workflow steps. Each invokes `scripts/ci-run.ts attempt` once; later steps run only after failure. The established `ciIt()` pass locks skip cases already passed, so only failed or uncompleted cases repeat. Inventory-enabled locks live beside the current run plan and are rebuilt from verified worker results before each attempt; stale machine locks and passes from failed hooks or interrupted workers cannot skip work. Android keeps its existing `/tmp/lock` behavior. Migration remains independent and keeps its per-attempt prepared-wallet setup.

The final report combines selected cases passed across these attempts. It requires the last runner to exit successfully and every selected case to have a verified current-run pass. All three failed attempts produce a failed job despite the attempts using `continue-on-error`. Each attempt retains worker events, process outcome and timing.

A recovered failure is reported as a recovery, with the first failed/missing case results and earlier errors retained. Missing reports, setup/teardown errors, incomplete workers, unexpected skips, stale identities or a nonzero final runner cannot produce a pass. The parent `e2e-status` requires every selected local or staging job and build to succeed. The migration workflow reports its own result.

## Cost controls and manual validation

Automatic runs use one local shard. Only manual dispatch can request two, partitioned by spec so a spec's fixture lifecycle stays together. Namespace supplies a separate Mac and backend instance per shard; every shard destroys its backend in cleanup. The local app is built once and reused. Staging and the independent migration workflow each use their own network build. Builds have a 40-minute limit; execution limits depend on the selected pack as described below. iOS build and diagnostic artifacts, including migration preparation logs, are retained for seven days; prepared wallet environments expire after one day. The shared preparation workflow preserves repository-default diagnostic retention for consumers that omit its optional override. Superseded runs cancel.

Compare `selection=full, shards=1` and `selection=full, shards=2` on the same app and E2E revisions. Inspect elapsed/build/setup/test time, first-pass success and billed Mac plus backend usage. Two shards are an experiment, not an assumed cost saving or a new automatic default.

Merge the E2E producer before the iOS consumer. Before merging, dispatch the existing iOS workflow from the development branch and explicitly select the E2E development branch. Validate base/full/automatic selection, failures/recovery and the optional shard experiment. The default branch's gate stays intact until the consumer is merged. No parallel shadow workflow is installed.

## Local checks

```sh
npm run inventory:check
npm run test:reporting
npx tsc --noEmit
```

The CLI workflow tests use controlled runner fixtures; they are not evidence that hosted Appium journeys passed. Actual hosted measurements and device results must name their revisions and environments.

Local shards with 6–30 cases allow 90 minutes per attempt within a 285-minute job and five-hour backend expiry. Larger shards allow 110 minutes per attempt within a 345-minute job and six-hour backend expiry. Both sizes allow three attempts. Base-sized local selections retain 40-minute attempts and a 135-minute job; the six-case staging pack also uses 90-minute attempts within 285 minutes. Individual migration cases retain 40-minute attempts and 135-minute jobs. The iOS selector owns these budgets and the shared runner seals its attempt limit into the run plan. Limits are ceilings, not reservations: success stops retries and cleanup destroys the backend immediately.

RN passphrase migration verifies the exact restored passphrase and the funded RN source balance before installing the native target. Native verification dismisses the optional USDT introduction before asserting balances, history, tags and persistence after relaunch. A malformed fixture or blocked screen remains a failure; neither is reported as a successful migration.
2 changes: 2 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
"name": "bitkit-e2e-tests",
"version": "1.0.0",
"scripts": {
"inventory:check": "node --experimental-strip-types scripts/e2e-inventory.ts check",
"test:reporting": "node --import tsx --test test/unit/inventory.test.ts test/unit/reporting.test.ts test/unit/reporting-cli.test.ts test/unit/reporter-events.test.ts test/unit/ci-retry.test.ts test/unit/wdio-scheduling.test.cjs test/unit/pinned-fixture-source.test.cjs",
"e2e:android": "PLATFORM=android wdio run ./wdio.conf.ts",
"e2e:ios": "PLATFORM=ios wdio run ./wdio.conf.ts",
"lint": "eslint './**/*.{ts,js}'",
Expand All @@ -18,6 +20,7 @@
"@wdio/cli": "^9.18.2",
"@wdio/local-runner": "^9.18.1",
"@wdio/mocha-framework": "^9.18.0",
"@wdio/reporter": "9.18.0",
"@wdio/spec-reporter": "^9.18.0",
"appium": "3.0.0",
"appium-uiautomator2-driver": "5.0.0",
Expand All @@ -32,6 +35,7 @@
"@radar/lnrpc": "^0.11.1-beta.1",
"bip21": "^3.0.0",
"bitcoin-json-rpc": "^1.4.1",
"bitcoinjs-lib": "6.1.7",
"lnurl": "0.26.2",
"rn-electrum-client": "^0.0.22"
},
Expand Down
Loading
Loading