@andrian.yablonskyy/thub-agent 1.1.1 → 1.1.3
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 +6 -13
- package/package.json +2 -2
- package/src/cli.js +30 -16
- package/src/config.js +4 -12
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ npx -y @andrian.yablonskyy/thub-agent run --type sw --download-file "$IMAGE_URL"
|
|
|
14
14
|
|
|
15
15
|
## Configuration
|
|
16
16
|
|
|
17
|
-
Read from flags (`--url`, `--key`), then environment (`THUB_URL`, `THUB_KEY`, `
|
|
17
|
+
Read from flags (`--url`, `--key`), then environment (`THUB_URL`, `THUB_KEY`, `THUB_USER`; the old names `--token`/`THUB_TOKEN` still work), then `~/.config/thub/agent.json`, then a bundled default. `url`/`token` are required by the time a command actually talks to the Coordinator; `group`/`user` are optional everywhere.
|
|
18
18
|
|
|
19
19
|
`npm install -g` creates `~/.config/thub/agent.json` for you (blank `url`/`token`, so nothing works until you set them) if it doesn't already exist — a re-install never overwrites it. Fill it in with `thub config set`:
|
|
20
20
|
|
|
@@ -24,7 +24,6 @@ thub config set key thk_... # your access key: from an admin (Users), or y
|
|
|
24
24
|
thub whoami # jane (user) <jane@example.com>
|
|
25
25
|
thub key show # its last characters, created, last used
|
|
26
26
|
thub key rotate # a new key; the old one stops at once (saved here if it came from here)
|
|
27
|
-
thub config set group 548ae4ae-... # optional default --group
|
|
28
27
|
thub config set user "Your Name" # optional default --user
|
|
29
28
|
```
|
|
30
29
|
|
|
@@ -36,7 +35,7 @@ thub status <jobId> Show status; follow log if running, verdict, test
|
|
|
36
35
|
thub cancel <jobId> Cancel a job
|
|
37
36
|
thub resources List resources and their status
|
|
38
37
|
thub jobs [--mine] [--state <s>] List recent jobs (a cli token: only its own; a ci token: all, or its own with --mine)
|
|
39
|
-
thub config set <key> <value> Save coordinator URL /
|
|
38
|
+
thub config set <key> <value> Save coordinator URL / key / default user locally
|
|
40
39
|
thub check-update Compare this Agent with the latest published version
|
|
41
40
|
thub self-update [--to <x.y.z>] Update this Agent with npm i -g
|
|
42
41
|
thub --version
|
|
@@ -49,8 +48,7 @@ Key options for `thub run`. **On the Client** names the environment variable the
|
|
|
49
48
|
| Option | Description | On the Client |
|
|
50
49
|
|---|---|---|
|
|
51
50
|
| `--type hw\|sw` | Required resource type. | `JOB_TYPE` |
|
|
52
|
-
| `--
|
|
53
|
-
| `--group <groupId>` | Restrict scheduling to resources that are members of this group. Falls back to `THUB_GROUP` / `thub config set group <id>`. | `JOB_GROUP` |
|
|
51
|
+
| `--label <l>` | Required labels (repeatable; a board is `--label board:<name>`): the job runs only on a runner that has all of them. Each 1–64 characters, no whitespace, commas or semicolons. | `JOB_LABEL`, `JOB_LABEL_<n>` |
|
|
54
52
|
| `--client <name\|id>` | Run on this specific Client (resource name or id) only; the job waits in that Client's queue even if other matching resources are idle. | `JOB_CLIENT` |
|
|
55
53
|
| `--user <name>` | Free-text job owner — a label, not an identity. Falls back to `THUB_USER` / `thub config set user <name>`. | `JOB_USER` |
|
|
56
54
|
| `--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_*`. | `JOB_COMMAND` |
|
|
@@ -90,7 +88,7 @@ Exit codes make the Agent usable as a CI step:
|
|
|
90
88
|
**Run a task** — download the firmware, check out the tests at a tag, flash and test (HW):
|
|
91
89
|
|
|
92
90
|
```bash
|
|
93
|
-
thub run --type hw --board
|
|
91
|
+
thub run --type hw --label board:nucleo-f401re \
|
|
94
92
|
--download-file "$IMAGE_URL" \
|
|
95
93
|
--git-repo https://github.com/yourorg/firmware-tests.git v1.4.0 \
|
|
96
94
|
--command 'st-flash --serial "$THUB_DUT_STLINK" --reset write "$THUB_DOWNLOAD_1" 0x08000000 && ./ci/test.sh "$@"' \
|
|
@@ -122,17 +120,12 @@ thub run --type sw --download-file https://does-not-exist.invalid/app.bin \
|
|
|
122
120
|
--command ./ci/test.sh --dry-run --wait
|
|
123
121
|
```
|
|
124
122
|
|
|
125
|
-
**Run
|
|
126
|
-
|
|
127
|
-
```bash
|
|
128
|
-
thub run --type sw --git-repo "$TESTS_REPO" --command ./ci/test.sh \
|
|
129
|
-
--group 548ae4ae-ac5b-401f-acaa-24bbe790e62d --wait
|
|
130
|
-
```
|
|
123
|
+
**Run in a resource group:** there's no `--group` option. An admin or maintainer gives your user (or a CI token) one group on the dashboard, and every job it submits runs only on that group's resources; `thub whoami` shows it.
|
|
131
124
|
|
|
132
125
|
**Label a job with its owner** — purely informational, shows up on the dashboard, in `thub jobs`, and on the Client's own console:
|
|
133
126
|
|
|
134
127
|
```bash
|
|
135
|
-
thub run --type hw --board
|
|
128
|
+
thub run --type hw --label board:nucleo-f401re \
|
|
136
129
|
--git-repo "$TESTS_REPO" --command ./ci/test.sh --user "Your Name" --wait
|
|
137
130
|
```
|
|
138
131
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andrian.yablonskyy/thub-agent",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
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.1.
|
|
15
|
+
"@andrian.yablonskyy/thub-common": "^1.1.2",
|
|
16
16
|
"commander": "^13.1.0"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
package/src/cli.js
CHANGED
|
@@ -21,7 +21,7 @@ const { Command, Option } = require('commander'),
|
|
|
21
21
|
splitArgs,
|
|
22
22
|
parseEnvList
|
|
23
23
|
} = require('@andrian.yablonskyy/thub-common'),
|
|
24
|
-
{ resolveConnection,
|
|
24
|
+
{ resolveConnection, resolveUser, writeConfigFile, readConfigFile, CONFIG_PATH } = require('./config'),
|
|
25
25
|
{ parseDurationSec } = require('./duration'),
|
|
26
26
|
{ followJob } = require('./streaming'),
|
|
27
27
|
{ applyRequestedUpdate, installAgent, version } = require('./self-update');
|
|
@@ -134,17 +134,27 @@ function fail(err){
|
|
|
134
134
|
process.exit(err.status && Number.isInteger(err.status) && err.status < 100 ? err.status : EXIT_CODES.USAGE);
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
// --label: the labels a runner must all have for the job (§7.1).
|
|
138
|
+
// Checked here against the Coordinator's rule, so a typo fails before the
|
|
139
|
+
// job is sent.
|
|
140
|
+
const LABEL_RE = /^[^\s,;]{1,64}$/;
|
|
141
|
+
function requiredLabels(opts){
|
|
142
|
+
const labels = opts.label,
|
|
143
|
+
bad = labels.filter((l) => !LABEL_RE.test(l));
|
|
144
|
+
if (bad.length){
|
|
145
|
+
const err = new Error(`Invalid label: ${bad.join(', ')} — 1–64 characters, no spaces, commas or semicolons`);
|
|
146
|
+
err.status = EXIT_CODES.USAGE;
|
|
147
|
+
throw err;
|
|
148
|
+
}
|
|
149
|
+
return [...new Set(labels)];
|
|
150
|
+
}
|
|
151
|
+
|
|
137
152
|
program
|
|
138
153
|
.command('run')
|
|
139
154
|
.description('Submit a test job and follow its log')
|
|
140
155
|
.requiredOption('--type <hw|sw>', 'Required resource type')
|
|
141
|
-
.option('--
|
|
142
|
-
|
|
143
|
-
.option(
|
|
144
|
-
'--group <groupId>',
|
|
145
|
-
'Restrict scheduling to resources that are members of this group (§13.1). ' +
|
|
146
|
-
'Overrides THUB_GROUP / config file; leave unset for an unconstrained run.'
|
|
147
|
-
)
|
|
156
|
+
.option('--label <label>', 'A label the runner must have (repeatable; the job runs only on a runner with all of them). ' +
|
|
157
|
+
'1–64 characters, no spaces, commas or semicolons', collectRepeatable, [])
|
|
148
158
|
.option(
|
|
149
159
|
'--client <nameOrId>',
|
|
150
160
|
'Run on this specific Client (resource name or id) only; the job waits in that Client\'s queue ' +
|
|
@@ -216,13 +226,13 @@ program
|
|
|
216
226
|
try {
|
|
217
227
|
const task = taskFromOptions(opts),
|
|
218
228
|
c = client(),
|
|
219
|
-
labels =
|
|
229
|
+
labels = requiredLabels(opts),
|
|
220
230
|
meta = Object.fromEntries(opts.meta.map((kv) => kv.split(/=(.*)/s).slice(0, 2))),
|
|
221
|
-
group = resolveGroup({ group: opts.group }),
|
|
222
231
|
user = resolveUser({ user: opts.user }),
|
|
223
232
|
|
|
224
233
|
spec = {
|
|
225
|
-
|
|
234
|
+
// No group: the Coordinator uses this key's, set on the dashboard (§13.1).
|
|
235
|
+
target: { type: opts.type, labels, ...(opts.client ? { client: opts.client } : {}) },
|
|
226
236
|
...task,
|
|
227
237
|
timeoutSec: parseDurationSec(opts.timeout),
|
|
228
238
|
...(opts.priority !== undefined ? { priority: opts.priority } : {}),
|
|
@@ -375,13 +385,15 @@ function formatBytes(bytes){
|
|
|
375
385
|
const config = program.command('config').description('Manage local Agent configuration');
|
|
376
386
|
config
|
|
377
387
|
.command('set')
|
|
378
|
-
.argument('<name>', 'url | key |
|
|
388
|
+
.argument('<name>', 'url | key | user')
|
|
379
389
|
.argument('<value>')
|
|
380
390
|
.action((name, value) => {
|
|
381
391
|
// `token` is the old name of `key`.
|
|
382
392
|
const setting = name === 'token' ? 'key' : name;
|
|
383
|
-
if (!['url', 'key', '
|
|
384
|
-
console.error(
|
|
393
|
+
if (!['url', 'key', 'user'].includes(setting)){
|
|
394
|
+
console.error(setting === 'group'
|
|
395
|
+
? 'Error: a job\'s group is set on the dashboard now (Users / CI tokens), not in the Agent'
|
|
396
|
+
: 'Error: the setting must be "url", "key", or "user"');
|
|
385
397
|
process.exit(EXIT_CODES.USAGE);
|
|
386
398
|
}
|
|
387
399
|
const { token: _old, ...current } = readConfigFile();
|
|
@@ -391,8 +403,10 @@ config
|
|
|
391
403
|
|
|
392
404
|
// Who this access key belongs to (§10.3).
|
|
393
405
|
function describeMe(me){
|
|
394
|
-
const role = me.user?.role ? me.user.role.charAt(0).toUpperCase() + me.user.role.slice(1) : ''
|
|
395
|
-
|
|
406
|
+
const role = me.user?.role ? me.user.role.charAt(0).toUpperCase() + me.user.role.slice(1) : '',
|
|
407
|
+
who = me.user ? `${me.user.username} (${role})${me.user.email ? ` <${me.user.email}>` : ''}` : `CI token "${me.name}"`;
|
|
408
|
+
// Older Coordinators don't say: then nothing about groups.
|
|
409
|
+
return me.group === undefined ? who : `${who}\nJobs run in: ${me.group ? `group ${me.group}` : 'any resource (no group)'}`;
|
|
396
410
|
}
|
|
397
411
|
|
|
398
412
|
program
|
package/src/config.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file packages/agent/src/config.js
|
|
3
|
-
* @description Agent config resolution: flags > env > config file > bundled default (url/token/
|
|
3
|
+
* @description Agent config resolution: flags > env > config file > bundled default (url/token/user)
|
|
4
4
|
*
|
|
5
5
|
* @author Andrian Yablonskyy
|
|
6
6
|
* @copyright Copyright (c) 2026 Andrian Yablonskyy. All rights reserved.
|
|
@@ -65,16 +65,8 @@ function resolveConnection(flags = {}){
|
|
|
65
65
|
return { url, token, keySource };
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
// Optional default for `--
|
|
69
|
-
//
|
|
70
|
-
// by group, same as if `groups` had never been used at all.
|
|
71
|
-
function resolveGroup(flags = {}){
|
|
72
|
-
const file = { ...readJsonFile(PACKAGE_DEFAULT_CONFIG_PATH), ...readConfigFile() };
|
|
73
|
-
return flags.group || process.env.THUB_GROUP || file.group || undefined;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Optional default for `--user` (§4.3/§7.1) — same flags > env > file
|
|
77
|
-
// precedence as group. Purely a label (who submitted this job), not an
|
|
68
|
+
// Optional default for `--user` (§4.3/§7.1) — flags > env > file, like
|
|
69
|
+
// url/token. Purely a label (who submitted this job), not an
|
|
78
70
|
// identity: unset is fine, and nothing on the Coordinator side enforces
|
|
79
71
|
// or authenticates it.
|
|
80
72
|
function resolveUser(flags = {}){
|
|
@@ -82,4 +74,4 @@ function resolveUser(flags = {}){
|
|
|
82
74
|
return flags.user || process.env.THUB_USER || file.user || undefined;
|
|
83
75
|
}
|
|
84
76
|
|
|
85
|
-
module.exports = { CONFIG_PATH, readConfigFile, writeConfigFile, resolveConnection,
|
|
77
|
+
module.exports = { CONFIG_PATH, readConfigFile, writeConfigFile, resolveConnection, resolveUser };
|