@andrian.yablonskyy/thub-agent 1.0.27 → 1.0.29
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.
- package/README.md +100 -6
- package/package.json +2 -2
- package/src/cli.js +9 -7
package/README.md
CHANGED
|
@@ -52,11 +52,11 @@ Key options for `thub run`:
|
|
|
52
52
|
| `--user <name>` | Free-text job owner — a label, not an identity. Falls back to `THUB_USER` / `thub config set user <name>`. |
|
|
53
53
|
| `--command <string>` | **Required.** The task's entry point: a shell command the Client runs (`sh -c`) in the task's work directory — the `--git-repo` checkout, else an empty directory — after preparing its inputs. Its exit code is the verdict. On HW it flashes the board itself (the Client doesn't); it gets `THUB_DUT_STLINK`/`_UART`/`_USB`/`_HOST`/`_CONTAINER`, `THUB_DOWNLOAD_<n>`, `THUB_DOWNLOADS_DIR`, `THUB_GIT_COMMIT`, `THUB_SUITE`, `THUB_META_*`. |
|
|
54
54
|
| `--download-file <url>` | A file the Client downloads before running the command (repeatable, `http(s)`), into the job's `downloads/` directory. |
|
|
55
|
-
| `--docker-image <name>` | SW only: a Docker image the Client runs as the DUT
|
|
55
|
+
| `--docker-image <name>` | SW only: a Docker image the Client runs as the **DUT** (an emulator the tests talk to, at `$THUB_DUT_HOST` / `$THUB_DUT_CONTAINER`) — any SW Client runs it; without one, an SW job has no DUT container. It's not where `--command` runs: to run the tests in an image, see [Docker](#docker). |
|
|
56
56
|
| `--git-repo <url> [<branch>\|<tag>\|<commit>]` | A git repository the Client clones (default ref: the default branch); the command runs in the checkout. |
|
|
57
57
|
| `--depth <n>` | With `--git-repo`: commits to fetch, default `1`; `0` = full history. |
|
|
58
58
|
| `--git-options <string>` | With `--git-repo`: extra git options placed between `git` and its subcommand on the Client, e.g. `'-c core.sshCommand="ssh -i ~/.ssh/lab_key -p 2222"'`. Shell-quoted (no shell run). Stored with the job, so reference key files rather than inlining secrets. |
|
|
59
|
-
| `--env <vars>` | Environment variables for every command the Client runs for the job (git
|
|
59
|
+
| `--env <vars>` | Environment variables for every command the Client runs for the job (git and `--command`): `NAME=value[,NAME=value]`, repeatable; `--env NAME` alone takes the value from your shell. Any names — none means anything to the Agent or the Client (only `THUB_*`, `GIT_TERMINAL_PROMPT`, `GIT_ALLOW_PROTOCOL` are refused). Every value is a secret: it reaches only the Client running the job; the Coordinator masks it and drops it when the job ends. |
|
|
60
60
|
| `--suite <name>` | Passed to the command as `THUB_SUITE`. |
|
|
61
61
|
| `--arg <value>` | Extra argument for the command, as `"$@"` (repeatable). |
|
|
62
62
|
| `--timeout <dur>` | e.g. `30m`, default `30m`. |
|
|
@@ -68,7 +68,7 @@ Key options for `thub run`:
|
|
|
68
68
|
| `--json` | Machine-readable output. |
|
|
69
69
|
|
|
70
70
|
- `thub run` prints the **job ID**, then streams logs until Ctrl-C. Ctrl-C detaches; the job keeps running on the Client.
|
|
71
|
-
- `thub status <jobId>` prints the current state; if active it keeps streaming, if done it prints the verdict and artifact download links.
|
|
71
|
+
- `thub status <jobId>` prints the current state; if active it keeps streaming, if done it prints the verdict and artifact download links. `--json` prints the job once and never follows it (see [Job status and PASS/FAIL](#job-status-and-passfail)).
|
|
72
72
|
|
|
73
73
|
Exit codes make the Agent usable as a CI step:
|
|
74
74
|
|
|
@@ -79,6 +79,7 @@ Exit codes make the Agent usable as a CI step:
|
|
|
79
79
|
| `2` | `ERROR`, `TIMEOUT`, or `LOST` |
|
|
80
80
|
| `3` | `CANCELED` |
|
|
81
81
|
| `4` | Usage, auth or connection error |
|
|
82
|
+
| `5` | `thub status <jobId> --json`: the job is still queued or running |
|
|
82
83
|
| `130` | Detached with Ctrl-C (job still running) |
|
|
83
84
|
|
|
84
85
|
## Examples
|
|
@@ -93,12 +94,12 @@ thub run --type hw --board nucleo-f401re \
|
|
|
93
94
|
--arg --junit --wait
|
|
94
95
|
```
|
|
95
96
|
|
|
96
|
-
**SW task
|
|
97
|
+
**SW task against a DUT emulator image of your own** (the command runs on the Client host and talks to the emulator):
|
|
97
98
|
|
|
98
99
|
```bash
|
|
99
|
-
thub run --type sw --docker-image
|
|
100
|
+
thub run --type sw --docker-image registry.lab.local:5000/dut-emulator:2026.08 \
|
|
100
101
|
--git-repo git@github.com:yourorg/firmware-tests.git main --depth 20 \
|
|
101
|
-
--command 'make test' --wait
|
|
102
|
+
--command 'make test DUT="$THUB_DUT_HOST"' --wait
|
|
102
103
|
```
|
|
103
104
|
|
|
104
105
|
**Associate a CI/CD job id with the internal job id:**
|
|
@@ -132,6 +133,99 @@ thub run --type hw --board nucleo-f401re \
|
|
|
132
133
|
--git-repo "$TESTS_REPO" --command ./ci/test.sh --user "Your Name" --wait
|
|
133
134
|
```
|
|
134
135
|
|
|
136
|
+
## Environment variables and secrets
|
|
137
|
+
|
|
138
|
+
`--env NAME=value[,NAME=value]` (repeatable) sets variables for every command the Client runs for the job: git and `--command`. The names are yours; none means anything to the Agent or the Client. `--env NAME` alone takes the value from your own environment, so a secret stays off the command line and out of CI logs:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
export API_TOKEN=…
|
|
142
|
+
thub run --type sw --git-repo "$TESTS_REPO" \
|
|
143
|
+
--env TARGET=staging --env API_TOKEN \
|
|
144
|
+
--command './run-tests.sh --target "$TARGET"' --wait
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**How the values are kept secret:**
|
|
148
|
+
|
|
149
|
+
- They reach **only the Client that runs the job**.
|
|
150
|
+
- The Agent API shows every value as `***`: `thub status --json`, `thub jobs --json`, even for your own job. The dashboard lists the names only.
|
|
151
|
+
- The Coordinator replaces the values with `***` once the job ends.
|
|
152
|
+
- A `--dry-run` shows every value as `***`, whatever its name.
|
|
153
|
+
|
|
154
|
+
Your command's output is the job's log, so don't print secrets. Everything else — `--command`, `--arg`, `--meta`, `--git-options` — is stored as is and visible, so never put secrets there.
|
|
155
|
+
|
|
156
|
+
## Docker
|
|
157
|
+
|
|
158
|
+
**Log in to a custom registry:** in `--command`, with credentials passed as `--env` under any names. Point `DOCKER_CONFIG` at the job's work directory first, so the login is deleted with the job rather than left in the Client user's `~/.docker` for later jobs:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
export DOCKER_PASSWORD=…
|
|
162
|
+
thub run --type hw \
|
|
163
|
+
--env DOCKER_REGISTRY=registry.lab.local:5000,DOCKER_USER=ci --env DOCKER_PASSWORD \
|
|
164
|
+
--command 'export DOCKER_CONFIG="$THUB_WORK_DIR/.docker" &&
|
|
165
|
+
echo "$DOCKER_PASSWORD" | docker login "$DOCKER_REGISTRY" --username "$DOCKER_USER" --password-stdin &&
|
|
166
|
+
docker pull "$DOCKER_REGISTRY/team/test-runner:1.4"' \
|
|
167
|
+
--wait
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**Pull an image as the DUT:** `--docker-image` is pulled by the Client before the command runs, as its service user. For a private registry, log in once on the Client host as that user: `sudo -u thub docker login registry.lab.local:5000`.
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
thub run --type sw --docker-image registry.lab.local:5000/dut-emulator:2026.08 \
|
|
174
|
+
--git-repo "$TESTS_REPO" --command './ci/test.sh --dut "$THUB_DUT_HOST"' --wait
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**Run the command inside a container:** `--command` starts on the Client host in the work directory (`$THUB_WORK_DIR`, the `--git-repo` checkout). Start the container from it, mounting that directory:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
thub run --type sw \
|
|
181
|
+
--env DOCKER_REGISTRY=registry.lab.local:5000,DOCKER_USER=ci --env DOCKER_PASSWORD \
|
|
182
|
+
--git-repo git@bitbucket.org:yourorg/web-ui-tests.git main \
|
|
183
|
+
--git-options '-c core.sshCommand="ssh -i /home/thub/.ssh/id_ed25519 -o StrictHostKeyChecking=accept-new"' \
|
|
184
|
+
--command 'export DOCKER_CONFIG="$THUB_WORK_DIR/.docker" &&
|
|
185
|
+
echo "$DOCKER_PASSWORD" | docker login "$DOCKER_REGISTRY" --username "$DOCKER_USER" --password-stdin &&
|
|
186
|
+
docker run --rm -v "$THUB_WORK_DIR:/work" -w /work "$DOCKER_REGISTRY/python:3.14" ./run-tests.sh' \
|
|
187
|
+
--wait
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**Clone the repository inside the image, with parameters from `--env`:**
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
thub run --type sw \
|
|
194
|
+
--env DOCKER_REGISTRY=registry.lab.local:5000,DOCKER_USER=ci --env DOCKER_PASSWORD \
|
|
195
|
+
--env TEST_IMAGE=registry.lab.local:5000/team/test-runner:1.4 \
|
|
196
|
+
--env REPO_URL=git@bitbucket.org:yourorg/web-ui-tests.git,REPO_REF=main \
|
|
197
|
+
--env 'GIT_SSH_COMMAND=ssh -i /root/.ssh/id_ed25519 -o StrictHostKeyChecking=accept-new' \
|
|
198
|
+
--command 'export DOCKER_CONFIG="$THUB_WORK_DIR/.docker" &&
|
|
199
|
+
echo "$DOCKER_PASSWORD" | docker login "$DOCKER_REGISTRY" --username "$DOCKER_USER" --password-stdin &&
|
|
200
|
+
docker run --rm -e REPO_URL -e REPO_REF -e GIT_SSH_COMMAND \
|
|
201
|
+
-v "$HOME/.ssh:/root/.ssh:ro" -v "$THUB_WORK_DIR/results:/results" \
|
|
202
|
+
"$TEST_IMAGE" sh -c "git clone --depth 1 --branch \"\$REPO_REF\" \"\$REPO_URL\" /src && cd /src && ./run-tests.sh --junit /results"' \
|
|
203
|
+
--wait
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
The Client host needs Docker and the Client's user in the `docker` group; SW Clients have both. Add `--dry-run` to see every command a job would run on the Client (every `--env` value shown as `***`) without running any. More in the main README, §7.2.
|
|
207
|
+
|
|
208
|
+
## Job status and PASS/FAIL
|
|
209
|
+
|
|
210
|
+
The verdict is `--command`'s exit code: `0` → **PASSED**, anything else → **FAILED**. ERROR, TIMEOUT, LOST and CANCELED mean the job didn't run to a verdict. JUnit XML written to `results/` or `artifacts/` in the work directory is uploaded and summed into the job's `summary`.
|
|
211
|
+
|
|
212
|
+
**In CI:** `thub run … --wait` exits with the verdict code (table above): `0` PASSED, `1` FAILED, `2` infrastructure, `3` canceled.
|
|
213
|
+
|
|
214
|
+
**Check a job:**
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
thub status M-00125 # state; follows the log while active; then "Verdict: …" and artifact links; exit = verdict
|
|
218
|
+
thub jobs --mine --state FAILED
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**From a script:** `thub status <jobId> --json` prints the job once, with `state`, `exit_code`, `summary`, `message` and `resource.name`. It exits with the verdict code, or `5` while the job is still queued or running:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
JOB=$(thub run --type sw --git-repo "$TESTS_REPO" --command ./ci/test.sh --detach --json | jq -r .jobId)
|
|
225
|
+
while thub status "$JOB" --json > job.json; [ $? -eq 5 ]; do sleep 10; done
|
|
226
|
+
jq -r '"\(.state) exit=\(.exit_code) failed=\(.summary.failed // 0)"' job.json # PASSED exit=0 failed=0
|
|
227
|
+
```
|
|
228
|
+
|
|
135
229
|
## GitHub Actions
|
|
136
230
|
|
|
137
231
|
```yaml
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andrian.yablonskyy/thub-agent",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.29",
|
|
4
4
|
"description": "TestHub Agent CLI — the single entry point CI/CD and developers use to submit and follow test jobs",
|
|
5
5
|
"bin": {
|
|
6
6
|
"thub": "./src/cli.js"
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"lint:fix": "eslint . --fix"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@andrian.yablonskyy/thub-common": "^1.0.
|
|
15
|
+
"@andrian.yablonskyy/thub-common": "^1.0.29",
|
|
16
16
|
"commander": "^13.1.0"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
package/src/cli.js
CHANGED
|
@@ -168,8 +168,8 @@ program
|
|
|
168
168
|
)
|
|
169
169
|
.option(
|
|
170
170
|
'--docker-image <name>',
|
|
171
|
-
'SW jobs only: a Docker image the Client runs as the DUT (e.g.
|
|
172
|
-
'
|
|
171
|
+
'SW jobs only: a Docker image the Client runs as the job\'s DUT container, next to --command (e.g. registry.lab:5000/emu:1); ' +
|
|
172
|
+
'pulled from the registry it names, else Docker Hub. Without it, an SW job has no DUT container'
|
|
173
173
|
)
|
|
174
174
|
.option(
|
|
175
175
|
'--git-repo <url...>',
|
|
@@ -184,9 +184,9 @@ program
|
|
|
184
184
|
)
|
|
185
185
|
.option(
|
|
186
186
|
'--env <vars>',
|
|
187
|
-
'Environment variables for every command the Client runs for the job (git,
|
|
187
|
+
'Environment variables for every command the Client runs for the job (git, --command): ' +
|
|
188
188
|
'NAME=value[,NAME=value] (repeatable; a value may contain commas); --env NAME alone takes its value from this shell. ' +
|
|
189
|
-
'
|
|
189
|
+
'Any names (except THUB_*, GIT_TERMINAL_PROMPT, GIT_ALLOW_PROTOCOL). ' +
|
|
190
190
|
'Values reach only the Client running the job; the Coordinator masks them and drops them when the job ends',
|
|
191
191
|
collectRepeatable,
|
|
192
192
|
[]
|
|
@@ -254,14 +254,16 @@ program
|
|
|
254
254
|
.command('status')
|
|
255
255
|
.description('Show status; follow log if running, show artifacts if done')
|
|
256
256
|
.argument('<jobId>')
|
|
257
|
-
.option('--json', '
|
|
257
|
+
.option('--json', 'Print the job as JSON once, without following it; exit code: its verdict, or 5 while it\'s still active', false)
|
|
258
258
|
.action(async (jobId, opts) => {
|
|
259
259
|
try {
|
|
260
260
|
const c = client(),
|
|
261
261
|
job = await c.get(`/jobs/${jobId}`);
|
|
262
|
-
|
|
262
|
+
// --json never follows: a script polls it, and tells "still running"
|
|
263
|
+
// (exit 5) from a verdict.
|
|
264
|
+
if (opts.json){
|
|
263
265
|
console.log(JSON.stringify(job));
|
|
264
|
-
return process.exit(exitCodeForJobState(job.state));
|
|
266
|
+
return process.exit(ACTIVE_JOB_STATES.has(job.state) ? EXIT_CODES.ACTIVE : exitCodeForJobState(job.state));
|
|
265
267
|
}
|
|
266
268
|
|
|
267
269
|
console.log(`Job ${job.id} — ${job.state}${job.resource ? ' on ' + job.resource.name : ''}`);
|