@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 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`, `THUB_GROUP`, `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.
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 / token / default group / default user locally
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
- | `--board <name>` / `--label <l>` | Required labels (repeatable). | `JOB_BOARD`; `JOB_LABEL`, `JOB_LABEL_<n>` |
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 nucleo-f401re \
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 on a specific resource group only:**
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 nucleo-f401re \
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.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.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, resolveGroup, resolveUser, writeConfigFile, readConfigFile, CONFIG_PATH } = require('./config'),
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('--board <name>', 'Shorthand for --label board:<name>')
142
- .option('--label <label>', 'Required label the resource must have (repeatable)', collectRepeatable, [])
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 = [...(opts.board ? [`board:${opts.board}`] : []), ...opts.label],
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
- target: { type: opts.type, labels, ...(group ? { group } : {}), ...(opts.client ? { client: opts.client } : {}) },
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 | group | user')
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', 'group', 'user'].includes(setting)){
384
- console.error('Error: the setting must be "url", "key", "group", or "user"');
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
- return me.user ? `${me.user.username} (${role})${me.user.email ? ` <${me.user.email}>` : ''}` : `CI token "${me.name}"`;
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/group/user)
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 `--group` (§4.3/§7.1) — same flags > env > file
69
- // precedence as url/token, but unset is fine: a job just runs unconstrained
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, resolveGroup, resolveUser };
77
+ module.exports = { CONFIG_PATH, readConfigFile, writeConfigFile, resolveConnection, resolveUser };