@leera.io/qa-runner 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/LICENSE +64 -0
  2. package/README.md +333 -0
  3. package/dist/src/appium/drivers.js +60 -0
  4. package/dist/src/appium/home.js +60 -0
  5. package/dist/src/appium/server.js +237 -0
  6. package/dist/src/builds/cache.js +133 -0
  7. package/dist/src/builds/cleanup.js +208 -0
  8. package/dist/src/builds/download.js +64 -0
  9. package/dist/src/builds/install.js +75 -0
  10. package/dist/src/builds/ios-app.js +99 -0
  11. package/dist/src/builds/unpack.js +347 -0
  12. package/dist/src/capabilities.js +93 -0
  13. package/dist/src/ci/builds.js +68 -0
  14. package/dist/src/ci/client.js +90 -0
  15. package/dist/src/ci/env.js +118 -0
  16. package/dist/src/ci/junit.js +79 -0
  17. package/dist/src/ci/run.js +320 -0
  18. package/dist/src/ci/summary.js +126 -0
  19. package/dist/src/cli/commands/builds.js +104 -0
  20. package/dist/src/cli/commands/ci.js +167 -0
  21. package/dist/src/cli/commands/config.js +83 -0
  22. package/dist/src/cli/commands/connect.js +70 -0
  23. package/dist/src/cli/commands/doctor.js +57 -0
  24. package/dist/src/cli/commands/service.js +165 -0
  25. package/dist/src/cli/commands/setup.js +193 -0
  26. package/dist/src/cli/commands/start.js +94 -0
  27. package/dist/src/cli/commands/update.js +189 -0
  28. package/dist/src/cli/commands/version.js +28 -0
  29. package/dist/src/cli/main.js +59 -0
  30. package/dist/src/cli/output.js +67 -0
  31. package/dist/src/cli.js +40 -0
  32. package/dist/src/client.js +146 -0
  33. package/dist/src/config.js +260 -0
  34. package/dist/src/debug.js +137 -0
  35. package/dist/src/devices/android/adb.js +251 -0
  36. package/dist/src/devices/android/avd.js +95 -0
  37. package/dist/src/devices/android/emulator.js +153 -0
  38. package/dist/src/devices/android/index.js +133 -0
  39. package/dist/src/devices/android/logcat.js +205 -0
  40. package/dist/src/devices/android/prepare.js +40 -0
  41. package/dist/src/devices/android/setup.js +162 -0
  42. package/dist/src/devices/android/system-dialog.js +78 -0
  43. package/dist/src/devices/desktop.js +145 -0
  44. package/dist/src/devices/ios/devicectl.js +87 -0
  45. package/dist/src/devices/ios/index.js +130 -0
  46. package/dist/src/devices/ios/prepare.js +39 -0
  47. package/dist/src/devices/ios/record.js +122 -0
  48. package/dist/src/devices/ios/settings.js +55 -0
  49. package/dist/src/devices/ios/setup.js +186 -0
  50. package/dist/src/devices/ios/simctl.js +201 -0
  51. package/dist/src/devices/ios/syslog.js +185 -0
  52. package/dist/src/devices/ios/wda.js +108 -0
  53. package/dist/src/devices/macos.js +248 -0
  54. package/dist/src/devices/manager.js +206 -0
  55. package/dist/src/devices/screen-record.js +95 -0
  56. package/dist/src/devices/tauri.js +189 -0
  57. package/dist/src/devices/types.js +6 -0
  58. package/dist/src/devices/windows.js +362 -0
  59. package/dist/src/doctor.js +394 -0
  60. package/dist/src/download.js +39 -0
  61. package/dist/src/drivers/android-appium.js +525 -0
  62. package/dist/src/drivers/common.js +64 -0
  63. package/dist/src/drivers/desktop-appium.js +282 -0
  64. package/dist/src/drivers/driver.js +15 -0
  65. package/dist/src/drivers/electron-playwright.js +566 -0
  66. package/dist/src/drivers/ios-appium.js +535 -0
  67. package/dist/src/drivers/mac2-appium.js +313 -0
  68. package/dist/src/drivers/registry.js +47 -0
  69. package/dist/src/drivers/snapshot/aria.js +351 -0
  70. package/dist/src/drivers/snapshot/format.js +106 -0
  71. package/dist/src/drivers/tauri-webdriver.js +678 -0
  72. package/dist/src/drivers/web-playwright.js +403 -0
  73. package/dist/src/drivers/webdriver/actions.js +328 -0
  74. package/dist/src/drivers/webdriver/desktop-keys.js +141 -0
  75. package/dist/src/drivers/webdriver/dom-locate.js +113 -0
  76. package/dist/src/drivers/webdriver/dom-snapshot.js +376 -0
  77. package/dist/src/drivers/webdriver/dom-tree.js +118 -0
  78. package/dist/src/drivers/webdriver/dom.js +213 -0
  79. package/dist/src/drivers/webdriver/ios-actions.js +299 -0
  80. package/dist/src/drivers/webdriver/ios-locate.js +121 -0
  81. package/dist/src/drivers/webdriver/locate.js +136 -0
  82. package/dist/src/drivers/webdriver/native-actions.js +175 -0
  83. package/dist/src/drivers/webdriver/native-locate.js +273 -0
  84. package/dist/src/drivers/webdriver/session.js +105 -0
  85. package/dist/src/drivers/webdriver/xml-tree-desktop.js +425 -0
  86. package/dist/src/drivers/webdriver/xml-tree-ios.js +314 -0
  87. package/dist/src/drivers/webdriver/xml-tree.js +393 -0
  88. package/dist/src/drivers/windows-appium.js +319 -0
  89. package/dist/src/errors.js +21 -0
  90. package/dist/src/executor.js +189 -0
  91. package/dist/src/home.js +76 -0
  92. package/dist/src/index.js +22 -0
  93. package/dist/src/log.js +69 -0
  94. package/dist/src/runner.js +498 -0
  95. package/dist/src/service/index.js +64 -0
  96. package/dist/src/service/launchd.js +85 -0
  97. package/dist/src/service/names.js +2 -0
  98. package/dist/src/service/schtasks.js +128 -0
  99. package/dist/src/service/systemd.js +65 -0
  100. package/dist/src/session/commands.js +165 -0
  101. package/dist/src/session/execute.js +232 -0
  102. package/dist/src/session/loop.js +148 -0
  103. package/dist/src/template.js +62 -0
  104. package/dist/src/types.js +7 -0
  105. package/dist/src/update/apply.js +223 -0
  106. package/dist/src/update/check.js +59 -0
  107. package/dist/src/update/manifest.js +93 -0
  108. package/dist/src/update/verify.js +65 -0
  109. package/dist/src/version.js +42 -0
  110. package/package.json +44 -0
package/LICENSE ADDED
@@ -0,0 +1,64 @@
1
+ Leera QA Runner License
2
+
3
+ Copyright (c) 2026 Leera AI Solutions LLP. All rights reserved.
4
+
5
+ The Leera QA Runner (the "Software") is proprietary software owned by Leera AI
6
+ Solutions LLP ("Leera"). It is licensed, not sold. By installing or running the
7
+ Software you agree to this license. If you do not agree, do not install or run
8
+ it.
9
+
10
+ 1. Grant. Leera grants you a free, non-exclusive, non-transferable,
11
+ non-sublicensable, revocable right to install and run unmodified copies of
12
+ the Software, in the form Leera distributes it, solely to execute tests
13
+ against and report results to a Leera workspace that you are authorised to
14
+ use, whether hosted by Leera or on a self-hosted Leera instance you are
15
+ licensed to operate. This includes evaluation and trial use on the same
16
+ terms. You may run as many copies as that purpose requires, including on
17
+ your own machines and in your continuous-integration systems.
18
+
19
+ 2. Restrictions. Except as this license expressly permits, you may not:
20
+ (a) copy, distribute, sell, rent, lease, sublicense, publish or otherwise
21
+ make the Software available to any third party, including as part of
22
+ another product or a hosted service (installing it from Leera's
23
+ official distribution channels on systems you control is permitted);
24
+ (b) modify, adapt, translate or create derivative works of the Software;
25
+ (c) reverse engineer, decompile or disassemble the Software, except to the
26
+ extent applicable law expressly permits this despite this restriction;
27
+ (d) use the Software with any service other than a Leera workspace, or to
28
+ build a competing product;
29
+ (e) remove or alter any copyright, trademark or other proprietary notices.
30
+ Third-party open-source components included with the Software are licensed
31
+ under their own terms, which govern those components.
32
+
33
+ 3. Ownership. Leera and its licensors retain all right, title and interest in
34
+ the Software, including all intellectual property rights. No rights are
35
+ granted other than those expressly stated in this license.
36
+
37
+ 4. No warranty. THE SOFTWARE IS PROVIDED "AS IS" AND "AS AVAILABLE", WITHOUT
38
+ WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY
39
+ WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND
40
+ NON-INFRINGEMENT. YOU ARE RESPONSIBLE FOR THE SYSTEMS, APPLICATIONS AND DATA
41
+ YOU RUN THE SOFTWARE AGAINST.
42
+
43
+ 5. Limitation of liability. TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW,
44
+ IN NO EVENT WILL LEERA OR ITS LICENSORS BE LIABLE FOR ANY INDIRECT,
45
+ INCIDENTAL, SPECIAL, CONSEQUENTIAL, EXEMPLARY OR PUNITIVE DAMAGES, OR FOR ANY
46
+ LOSS OF DATA, PROFITS, REVENUE OR BUSINESS, ARISING OUT OF OR IN CONNECTION
47
+ WITH THE SOFTWARE OR THIS LICENSE, HOWEVER CAUSED AND UNDER ANY THEORY OF
48
+ LIABILITY, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. LEERA'S TOTAL
49
+ LIABILITY UNDER THIS LICENSE IS LIMITED AS SET OUT IN THE AGREEMENT UNDER
50
+ WHICH YOU USE LEERA; IF NO SUCH AGREEMENT APPLIES, IT WILL NOT EXCEED THE
51
+ AMOUNTS YOU PAID LEERA FOR THE RELEVANT WORKSPACE IN THE TWELVE MONTHS BEFORE
52
+ THE EVENT GIVING RISE TO THE CLAIM.
53
+
54
+ 6. Termination. This license ends automatically if you breach it, and Leera
55
+ may end it on notice at any time. It also ends for a workspace when your
56
+ right to use that workspace ends. On termination you must stop using the
57
+ Software and delete all copies in your possession or control. Sections 2 to
58
+ 7 survive termination.
59
+
60
+ 7. General. If you have a separate written agreement with Leera covering the
61
+ Software, that agreement prevails where it conflicts with this license. If
62
+ any provision is held unenforceable, the rest remains in effect.
63
+
64
+ Contact: contact@leera.io
package/README.md ADDED
@@ -0,0 +1,333 @@
1
+ # leera-qa-runner
2
+
3
+ The Leera QA runner runs your automated test cases on machines you own — a laptop, a
4
+ Mac mini under a desk, a Linux server or your CI — and reports step results, screenshots,
5
+ video and debug logs back to your Leera workspace.
6
+
7
+ It registers with your workspace using a **runner token** (`pm_run_…`), polls for queued
8
+ jobs, runs each recorded script and uploads the evidence. Nothing listens for incoming
9
+ connections: the runner only makes outgoing HTTPS requests to your server.
10
+
11
+ It runs **web** tests in Chromium, **Android** tests on emulators and USB phones, **iOS** tests
12
+ on simulators and iPhones (Mac only), and desktop apps built with **Electron**, **Tauri**, or
13
+ native **Windows** and **macOS** toolkits. Mobile and native desktop drivers use Appium, bundled
14
+ with the installers. See [test runners](https://github.com/leera-app/leera-qa-runner/blob/main/docs/test-runners.md)
15
+ for each platform's prerequisites and current limitations.
16
+
17
+ Full guides: [test runners](https://github.com/leera-app/leera-qa-runner/blob/main/docs/test-runners.md) and
18
+ [authoring automated test cases](https://github.com/leera-app/leera-qa-runner/blob/main/docs/qa-automation-authoring.md).
19
+
20
+ - [Install](#install)
21
+ - [Connect and run](#connect-and-run)
22
+ - [Run in the background](#run-in-the-background)
23
+ - [Check the machine: doctor](#check-the-machine-doctor)
24
+ - [Android](#android)
25
+ - [Configuration](#configuration)
26
+ - [Commands and exit codes](#commands-and-exit-codes)
27
+ - [Docker](#docker)
28
+ - [Uninstall](#uninstall)
29
+
30
+ ## Install
31
+
32
+ Every installer bundles its own Node.js; you do not need Node installed. Downloads are
33
+ listed on the [releases page](https://github.com/leera-app/leera-qa-runner/releases) with a
34
+ `SHA256SUMS` file, and the install scripts verify it.
35
+
36
+ ### macOS (Apple silicon and Intel)
37
+
38
+ ```sh
39
+ curl -fsSL https://raw.githubusercontent.com/leera-app/leera-qa-runner/main/install/install.sh | sh
40
+ ```
41
+
42
+ or download `leera-qa-runner-macos-arm64.pkg` (Apple silicon) / `leera-qa-runner-macos-x64.pkg`
43
+ (Intel) and open it. The package installs to `/usr/local/lib/leera-qa-runner/<version>` and
44
+ links `/usr/local/bin/leera-qa-runner`.
45
+
46
+ ### Linux (x64 and arm64)
47
+
48
+ ```sh
49
+ curl -fsSL https://raw.githubusercontent.com/leera-app/leera-qa-runner/main/install/install.sh | sh
50
+ ```
51
+
52
+ No root needed: it installs under `~/.local/lib/leera-qa-runner/<version>` and links
53
+ `~/.local/bin/leera-qa-runner` (change with `--prefix DIR`). Make sure `~/.local/bin` is on
54
+ your `PATH`. Chromium needs some system libraries; if a job cannot launch the browser, run the
55
+ `sudo … install-deps chromium` command that `leera-qa-runner setup browsers` prints.
56
+
57
+ Manual install: download `leera-qa-runner-linux-<arch>.tar.gz`, extract it anywhere and run
58
+ `leera-qa-runner-linux-<arch>/bin/leera-qa-runner`.
59
+
60
+ ### Windows (x64)
61
+
62
+ In PowerShell:
63
+
64
+ ```powershell
65
+ irm https://raw.githubusercontent.com/leera-app/leera-qa-runner/main/install/install.ps1 | iex
66
+ ```
67
+
68
+ or download and run `leera-qa-runner-windows-x64.exe`. It installs for the current user
69
+ (no administrator rights) into `%LOCALAPPDATA%\Programs\leera-qa-runner` and adds it to your
70
+ `PATH`; open a new terminal afterwards. The installer is not code-signed yet, so Windows
71
+ SmartScreen may warn before it runs ("More info" → "Run anyway").
72
+
73
+ ### npm
74
+
75
+ With Node.js 22.12 or newer:
76
+
77
+ ```sh
78
+ npm install -g @leera.io/qa-runner
79
+ leera-qa-runner setup browsers
80
+ ```
81
+
82
+ Appium and the UiAutomator2 driver are optional dependencies: installing with
83
+ `--omit=optional` gives a web-only runner.
84
+
85
+ ### Install options
86
+
87
+ `install.sh` accepts `--version X`, `--prefix DIR` (Linux), `--url URL`, `--token TOKEN`,
88
+ `--name NAME`, `--service`, `--no-browsers` and `--base-url URL` (a mirror of the release
89
+ assets). `install.ps1` accepts `-Version`, `-Url`, `-Token`, `-Name`, `-Service`,
90
+ `-NoBrowsers`, `-Portable` (unpack the `.zip` instead of running the installer) and `-BaseUrl`. Prefer passing the token through the `RUNNER_TOKEN`
91
+ environment variable so it does not land in your shell history:
92
+
93
+ ```sh
94
+ curl -fsSL https://raw.githubusercontent.com/leera-app/leera-qa-runner/main/install/install.sh \
95
+ | RUNNER_TOKEN=pm_run_… sh -s -- --url https://app.example.com --service
96
+ ```
97
+
98
+ ## Connect and run
99
+
100
+ 1. In your workspace, open **Settings → Test runners**, create a runner pool and copy a
101
+ runner token.
102
+ 2. Install Chromium (the installers do this for you):
103
+
104
+ ```sh
105
+ leera-qa-runner setup browsers
106
+ ```
107
+
108
+ 3. Connect. The token is asked for without echo:
109
+
110
+ ```sh
111
+ leera-qa-runner connect --url https://app.example.com --name lab-mac
112
+ ```
113
+
114
+ In scripts, pipe it instead: `printf '%s' "$TOKEN" | leera-qa-runner connect --url … --token-stdin`.
115
+ `connect` stores the URL in `~/.leera-qa-runner/config.json`, the token in
116
+ `~/.leera-qa-runner/token` (readable only by you) and runs `doctor`.
117
+
118
+ 4. Run it in this terminal:
119
+
120
+ ```sh
121
+ leera-qa-runner start
122
+ ```
123
+
124
+ or [install the background service](#run-in-the-background). The runner appears on the
125
+ Test runners page with its OS, architecture and version; queue an automated run and
126
+ watch it pick up the job. Press Ctrl-C to stop after the current step.
127
+
128
+ `start` options: `--once` (exit after one job), `--headed` (show the browser),
129
+ `--labels a,b` (exact label list), `--concurrency N` (browser jobs at once) and
130
+ `--run-id N` (only take jobs from one test run).
131
+
132
+ ## Run in the background
133
+
134
+ ```sh
135
+ leera-qa-runner service install # install and start; starts again at login
136
+ leera-qa-runner service status
137
+ leera-qa-runner service logs -f
138
+ leera-qa-runner service restart
139
+ leera-qa-runner service stop
140
+ leera-qa-runner service start
141
+ leera-qa-runner service uninstall
142
+ ```
143
+
144
+ The service runs as **your user, in your login session**, so it can open browsers and
145
+ Android emulators (and, later, simulators and app windows):
146
+
147
+ | OS | What is installed |
148
+ |---|---|
149
+ | macOS | LaunchAgent `~/Library/LaunchAgents/io.leera.qa-runner.plist` (starts at login, restarted if it exits). Keep the Mac logged in; a locked screen is fine for browser jobs. |
150
+ | Windows | Scheduled task `leera-qa-runner`, at logon, in your interactive session, with limited rights; restarted on failure. |
151
+ | Linux | systemd user unit `~/.config/systemd/user/leera-qa-runner.service`. `loginctl enable-linger` keeps it running after you log out (may need an administrator). |
152
+
153
+ Services start without your shell profile, so `service install` saves `PATH` and a few other
154
+ variables (`JAVA_HOME`, `ANDROID_HOME`, `ANDROID_SDK_ROOT`, `PLAYWRIGHT_BROWSERS_PATH`,
155
+ proxy variables) into the configuration's `env`. Re-run `service install` after changing them.
156
+
157
+ Logs are JSON lines in `~/.leera-qa-runner/logs/runner.log`, rotated at 20 MB with five old
158
+ files kept.
159
+
160
+ ## Check the machine: doctor
161
+
162
+ ```sh
163
+ leera-qa-runner doctor # human-readable
164
+ leera-qa-runner doctor --json # for scripts
165
+ leera-qa-runner doctor --fix # fix permissions, install Chromium if missing
166
+ leera-qa-runner doctor --platform web
167
+ leera-qa-runner doctor --platform android
168
+ ```
169
+
170
+ It checks Node.js, the runner home and its permissions, the server URL, the token (asking the
171
+ server whether it accepts it, without registering a runner), Chromium, and for Android: Appium,
172
+ the SDK, adb, the emulator, build-tools, Java, KVM on Linux and unauthorised phones. It exits `0` when
173
+ ready and `4` when something marked `[fail]` needs fixing, with the command that fixes it.
174
+
175
+ ## Android
176
+
177
+ 1. Install the Android SDK (Android Studio, or the command-line tools) and Java 17+, and set
178
+ `ANDROID_HOME` (or `"android": {"sdk_path": "…"}` in `config.json`). On Linux, emulators
179
+ need `/dev/kvm` and your user in the `kvm` group. For a phone, enable USB debugging and accept
180
+ the prompt on the phone.
181
+ 2. Prepare the runner:
182
+
183
+ ```sh
184
+ leera-qa-runner setup android # Appium home, SDK check, device list
185
+ leera-qa-runner setup android --install-sdk # also install SDK packages and create an emulator
186
+ leera-qa-runner doctor --platform android
187
+ ```
188
+
189
+ `--install-sdk` uses the SDK's own `sdkmanager`, so the SDK directory must already have
190
+ `platform-tools` and `cmdline-tools/latest`; otherwise it prints how to get them. Re-run
191
+ `service install` afterwards so the service sees `ANDROID_HOME` and `JAVA_HOME`.
192
+
193
+ 3. Upload an `.apk` on the **App builds** page (app bundles, `.aab`, are refused), set the
194
+ package id and build on the test environment's **Apps**, create a run with platform
195
+ *Android* and queue it on this runner's pool.
196
+
197
+ The runner advertises phones (`android`, `android-real`), running emulators and emulators that
198
+ exist but are off (`android`, `android-emulator`); an emulator is booted when a job needs it and
199
+ stays booted. Each job installs the build, clears the app's data, records an MP4 when asked
200
+ (runs over 3 minutes need `ffmpeg` on `PATH`) and captures the app's logcat warnings, errors and
201
+ crashes per step. The same devices serve **device sessions**, in which an AI agent explores the
202
+ app over MCP before writing a script. Details: [Android](https://github.com/leera-app/leera-qa-runner/blob/main/docs/test-runners.md#android),
203
+ [device sessions](https://github.com/leera-app/leera-qa-runner/blob/main/docs/qa-automation-authoring.md#device-sessions).
204
+
205
+ ## Configuration
206
+
207
+ Settings are read in this order; the first one set wins:
208
+
209
+ 1. command-line flags
210
+ 2. `RUNNER_*` environment variables
211
+ 3. legacy `LEERA_URL` / `LEERA_RUNNER_TOKEN`
212
+ 4. `~/.leera-qa-runner/config.json` (and the `token` file)
213
+ 5. defaults
214
+
215
+ ```sh
216
+ leera-qa-runner config list
217
+ leera-qa-runner config get url
218
+ leera-qa-runner config set concurrency.web 2
219
+ leera-qa-runner config unset proxy
220
+ leera-qa-runner config set-token # replace the token (prompted, or --token-stdin)
221
+ leera-qa-runner config path
222
+ ```
223
+
224
+ | Config key | Environment variable | Default | Meaning |
225
+ |---|---|---|---|
226
+ | `url` | `RUNNER_URL` (`LEERA_URL`) | — | Your workspace server, e.g. `https://app.example.com` |
227
+ | token (`config set-token`) | `RUNNER_TOKEN` (`LEERA_RUNNER_TOKEN`) | — | Runner pool token, `pm_run_…` |
228
+ | `name` | `RUNNER_NAME` | host name | Name shown on the Test runners page |
229
+ | `labels.extra` | — | — | Labels added to the detected ones (comma separated) |
230
+ | `labels.exclude` | — | — | Detected labels to leave out |
231
+ | — | `RUNNER_LABELS` | detected | Exact label list; replaces detection (`--labels` too) |
232
+ | `concurrency.web` | `RUNNER_CONCURRENCY` | `1` | Browser jobs at once (`--concurrency`) |
233
+ | `headless` | `RUNNER_HEADLESS` | `true` | Run Chromium without a window (`--headed` overrides) |
234
+ | `proxy` | `RUNNER_PROXY` | — | Proxy server for the browser |
235
+ | `action_timeout_ms` | `RUNNER_ACTION_TIMEOUT_MS` | `15000` | Time limit per action |
236
+ | `job_timeout_ms` | `RUNNER_JOB_TIMEOUT_MS` | `900000` | Time limit per job; later steps are blocked |
237
+ | `android.sdk_path` | — (`ANDROID_HOME`) | detected | Android SDK directory, absolute path; wins over `ANDROID_HOME` |
238
+ | `builds.allow_local_paths` | `RUNNER_ALLOW_LOCAL_PATHS` | `false` | Install apps from a file path on this machine (install `path`); off: such jobs are blocked |
239
+ | — | `RUNNER_ONCE` | `false` | Exit after one job (`--once`) |
240
+ | — | `RUNNER_RUN_ID` | — | Only take jobs from this test run (`--run-id`) |
241
+ | `env.NAME` | — | — | Variables applied when the service starts |
242
+ | — | `LEERA_RUNNER_HOME` | `~/.leera-qa-runner` | Where configuration, token, logs and browsers live |
243
+ | — | `PLAYWRIGHT_BROWSERS_PATH` | `~/.leera-qa-runner/browsers` when present | Where Chromium is installed |
244
+
245
+ Labels route jobs: a run that requires labels only goes to runners that have all of them. The
246
+ runner detects `web-chromium` when Chromium is installed, and `android`, `android-emulator` and
247
+ `android-real` for its Android devices.
248
+
249
+ ### Files in the runner home
250
+
251
+ | Path | Contents |
252
+ |---|---|
253
+ | `config.json` | Settings above (mode 0600) |
254
+ | `token` | The runner token (mode 0600; narrowed again if someone widens it) |
255
+ | `state.json` | pid, runner id and running jobs, for `service status` |
256
+ | `install.json` | How the runner was installed (`method`: pkg, exe, tar, brew, winget), read by `update` |
257
+ | `versions/` | Versions installed by `update`; `current` (a symlink, `current.txt` on Windows) names the active one |
258
+ | `logs/runner.log` | Service log, JSON lines |
259
+ | `browsers/` | Chromium installed by `setup browsers` |
260
+ | `appium/` | Appium home with the UiAutomator2 driver, seeded by `setup android` |
261
+ | `cache/builds/` | Downloaded app builds by SHA-256 (up to 10 GB) |
262
+
263
+ ## Commands and exit codes
264
+
265
+ | Command | What it does |
266
+ |---|---|
267
+ | `start [--once] [--service] [--headed] [--labels a,b] [--concurrency N] [--run-id N]` | Register and run jobs until stopped |
268
+ | `connect --url URL [--token-stdin] [--name NAME]` | Save URL and token, then run `doctor` |
269
+ | `doctor [--json] [--fix] [--offline] [--platform web\|android]` | Check the machine |
270
+ | `setup browsers` | Install Chromium |
271
+ | `setup android [--install-sdk] [--image ID] [--avd NAME]` | Prepare Appium and the Android SDK, list devices |
272
+ | `service install\|uninstall\|start\|stop\|restart\|status\|logs [-f]` | Background service |
273
+ | `config get\|set\|unset\|list\|path\|set-token` | Stored configuration |
274
+ | `version [--json]` | Print the version |
275
+ | `update [--check] [--version X] [--yes]` | Update the runner; `--check` exits 10 when an update is available |
276
+
277
+ | Exit code | Meaning |
278
+ |---|---|
279
+ | 0 | OK |
280
+ | 1 | Error |
281
+ | 2 | Usage or configuration error (unknown option, missing URL or token) |
282
+ | 3 | The server rejected the runner token — create a new one and run `config set-token` |
283
+ | 4 | The machine is not ready (`doctor` found a failing check) |
284
+ | 5 | The server needs a newer runner version — update the runner |
285
+ | 130 | Interrupted (Ctrl-C) |
286
+
287
+ ## Update
288
+
289
+ ```sh
290
+ leera-qa-runner update --check # exit 0: up to date, 10: an update is available
291
+ leera-qa-runner update # asks first; --yes skips the question, --version X picks a version
292
+ ```
293
+
294
+ `update` follows the way the runner was installed. Package, installer and tarball installs
295
+ download `leera-qa-runner-<os>-<arch>.tar.gz` (`.zip` on Windows) from the release, check it
296
+ against the release manifest `releases.json` (its Ed25519 signature is required when the build
297
+ carries a release key, and the SHA-256 always), unpack it into `~/.leera-qa-runner/versions/<version>`,
298
+ switch `versions/current` to it, keep the two newest versions and restart the background service —
299
+ no administrator rights. The installed launcher runs `versions/current` whenever it is newer
300
+ than the version it was installed with. npm, Homebrew and winget installs print (or, with
301
+ `--yes`, run) their own upgrade command; Docker users pull the new image. `RUNNER_UPDATE_BASE`
302
+ points `update` at a mirror of the releases page. A running runner logs once a day when the server
303
+ reports a newer version.
304
+
305
+ ## Docker
306
+
307
+ For servers and CI without a desktop, the web-only image keeps working as before (it cannot
308
+ run Android jobs):
309
+
310
+ ```sh
311
+ docker run -d --restart unless-stopped \
312
+ -e RUNNER_URL=https://app.example.com \
313
+ -e RUNNER_TOKEN=pm_run_… \
314
+ -e RUNNER_NAME=docker-1 \
315
+ ghcr.io/leera-app/leera-qa-runner:latest
316
+ ```
317
+
318
+ It reads the same `RUNNER_*` variables.
319
+
320
+ ## Uninstall
321
+
322
+ 1. `leera-qa-runner service uninstall` (if you installed the service).
323
+ 2. Remove the program:
324
+ - macOS package: `sudo rm -rf /usr/local/lib/leera-qa-runner /usr/local/bin/leera-qa-runner && sudo pkgutil --forget io.leera.qa-runner`
325
+ - Linux: `rm -rf ~/.local/lib/leera-qa-runner ~/.local/bin/leera-qa-runner`
326
+ - Windows: *Settings → Apps → Leera QA Runner → Uninstall*
327
+ - npm: `npm uninstall -g @leera.io/qa-runner`
328
+ 3. Remove its data: `rm -rf ~/.leera-qa-runner` (Windows: `%USERPROFILE%\.leera-qa-runner`).
329
+ 4. Delete the runner from the Test runners page, or revoke its token.
330
+
331
+ ## License
332
+
333
+ Proprietary — see [LICENSE](LICENSE). Free to use with a Leera workspace.
@@ -0,0 +1,60 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import { dirname } from 'node:path';
4
+ export const APPIUM_DRIVERS = [
5
+ {
6
+ name: 'uiautomator2',
7
+ pkg: 'appium-uiautomator2-driver',
8
+ automationName: 'UiAutomator2',
9
+ platforms: ['android'],
10
+ hosts: ['darwin', 'linux', 'win32'],
11
+ },
12
+ {
13
+ name: 'xcuitest',
14
+ pkg: 'appium-xcuitest-driver',
15
+ automationName: 'XCUITest',
16
+ platforms: ['ios'],
17
+ hosts: ['darwin'],
18
+ },
19
+ {
20
+ name: 'mac2',
21
+ pkg: 'appium-mac2-driver',
22
+ automationName: 'Mac2',
23
+ platforms: ['macos'],
24
+ hosts: ['darwin'],
25
+ },
26
+ {
27
+ name: 'windows',
28
+ pkg: 'appium-windows-driver',
29
+ automationName: 'Windows',
30
+ platforms: ['windows'],
31
+ hosts: ['win32'],
32
+ },
33
+ ];
34
+ const require = createRequire(import.meta.url);
35
+ /** Finds an installed package next to the runner (optional dependencies may be absent). */
36
+ export function resolvePackage(pkg, resolve = require.resolve) {
37
+ try {
38
+ const manifest = resolve(`${pkg}/package.json`);
39
+ const parsed = JSON.parse(readFileSync(manifest, 'utf8'));
40
+ return { name: pkg, version: parsed.version ?? '0.0.0', dir: dirname(manifest) };
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ }
46
+ /** Drivers installed in the runner's node_modules that work on this host. */
47
+ export function bundledDrivers(host = process.platform, resolve) {
48
+ const out = [];
49
+ for (const spec of APPIUM_DRIVERS) {
50
+ if (!spec.hosts.includes(host))
51
+ continue;
52
+ const found = resolvePackage(spec.pkg, resolve);
53
+ if (found)
54
+ out.push({ ...spec, version: found.version, dir: found.dir });
55
+ }
56
+ return out;
57
+ }
58
+ export function driverFor(platform) {
59
+ return APPIUM_DRIVERS.find((spec) => spec.platforms.includes(platform));
60
+ }
@@ -0,0 +1,60 @@
1
+ import { existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
2
+ import { join, resolve } from 'node:path';
3
+ /** `~/.leera-qa-runner/appium`: Appium's APPIUM_HOME for this runner, never `~/.appium`. */
4
+ export function appiumHomeDir(paths) {
5
+ return join(paths.root, 'appium');
6
+ }
7
+ /** Appium caches the extensions it found here; deleting it makes Appium rescan node_modules. */
8
+ export function manifestPath(home) {
9
+ return join(home, 'node_modules', '.cache', 'appium', 'extensions.yaml');
10
+ }
11
+ /**
12
+ * Seeds APPIUM_HOME with the drivers bundled in the runner's node_modules.
13
+ *
14
+ * Instead of `appium driver install --source=local` (which runs `npm link`, needs npm and
15
+ * writes to npm's global prefix), each driver is linked into `APPIUM_HOME/node_modules` and
16
+ * Appium's extension manifest cache is dropped whenever the bundled versions change, so
17
+ * Appium rescans and records them itself on the next start. Node resolves a linked driver
18
+ * through its real path, so it loads the same `appium` package the server runs from.
19
+ */
20
+ export function seedAppiumHome(home, appium, drivers) {
21
+ const modules = join(home, 'node_modules');
22
+ mkdirSync(modules, { recursive: true, mode: 0o700 });
23
+ const pkgJson = join(home, 'package.json');
24
+ if (!existsSync(pkgJson))
25
+ writeFileSync(pkgJson, `${JSON.stringify({ private: true, description: 'Appium home for the QA runner' }, null, 2)}\n`);
26
+ const seedFile = join(home, 'seed.json');
27
+ const wanted = { appium: appium.version, drivers: Object.fromEntries(drivers.map((d) => [d.name, `${d.version}@${d.dir}`])) };
28
+ let previous = null;
29
+ try {
30
+ previous = JSON.parse(readFileSync(seedFile, 'utf8'));
31
+ }
32
+ catch {
33
+ previous = null;
34
+ }
35
+ let changed = false;
36
+ for (const driver of drivers) {
37
+ const link = join(modules, driver.pkg);
38
+ if (linkTarget(link) !== resolve(driver.dir)) {
39
+ rmSync(link, { recursive: true, force: true });
40
+ symlinkSync(driver.dir, link, process.platform === 'win32' ? 'junction' : 'dir');
41
+ changed = true;
42
+ }
43
+ }
44
+ if (changed || JSON.stringify(previous) !== JSON.stringify(wanted) || !existsSync(manifestPath(home))) {
45
+ rmSync(manifestPath(home), { force: true });
46
+ writeFileSync(seedFile, `${JSON.stringify(wanted, null, 2)}\n`);
47
+ changed = true;
48
+ }
49
+ return { home, changed, drivers: drivers.map((d) => ({ name: d.name, version: d.version })) };
50
+ }
51
+ function linkTarget(path) {
52
+ try {
53
+ if (!lstatSync(path).isSymbolicLink())
54
+ return null;
55
+ return resolve(join(path, '..'), readlinkSync(path));
56
+ }
57
+ catch {
58
+ return null;
59
+ }
60
+ }