@andrian.yablonskyy/thub-agent 1.0.33 → 1.1.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.
- package/README.md +8 -5
- package/package.json +2 -2
- package/src/cli.js +95 -12
- package/src/config.js +11 -5
package/README.md
CHANGED
|
@@ -14,13 +14,16 @@ npx -y @andrian.yablonskyy/thub-agent run --type sw --download-file "$IMAGE_URL"
|
|
|
14
14
|
|
|
15
15
|
## Configuration
|
|
16
16
|
|
|
17
|
-
Read from flags, then environment (`THUB_URL`, `
|
|
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.
|
|
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
|
|
|
21
21
|
```bash
|
|
22
22
|
thub config set url https://thub.example.com
|
|
23
|
-
thub config set
|
|
23
|
+
thub config set key thk_... # your access key: from an admin (Users), or your dashboard profile
|
|
24
|
+
thub whoami # jane (user) <jane@example.com>
|
|
25
|
+
thub key show # its last characters, created, last used
|
|
26
|
+
thub key rotate # a new key; the old one stops at once (saved here if it came from here)
|
|
24
27
|
thub config set group 548ae4ae-... # optional default --group
|
|
25
28
|
thub config set user "Your Name" # optional default --user
|
|
26
29
|
```
|
|
@@ -29,10 +32,10 @@ thub config set user "Your Name" # optional default --user
|
|
|
29
32
|
|
|
30
33
|
```
|
|
31
34
|
thub run [options] Submit a test job and follow its log
|
|
32
|
-
thub status <jobId> Show status; follow log if running, verdict
|
|
35
|
+
thub status <jobId> Show status; follow log if running, verdict, test counts and artifacts if done
|
|
33
36
|
thub cancel <jobId> Cancel a job
|
|
34
37
|
thub resources List resources and their status
|
|
35
|
-
thub jobs [--mine] [--state <s>] List recent jobs
|
|
38
|
+
thub jobs [--mine] [--state <s>] List recent jobs (a cli token: only its own; a ci token: all, or its own with --mine)
|
|
36
39
|
thub config set <key> <value> Save coordinator URL / token / default group / default user locally
|
|
37
40
|
thub check-update Compare this Agent with the latest published version
|
|
38
41
|
thub self-update [--to <x.y.z>] Update this Agent with npm i -g
|
|
@@ -249,7 +252,7 @@ test-sw:
|
|
|
249
252
|
runs-on: ubuntu-latest
|
|
250
253
|
env:
|
|
251
254
|
THUB_URL: https://thub.example.com
|
|
252
|
-
|
|
255
|
+
THUB_KEY: ${{ secrets.THUB_CI_TOKEN }} # a CI token (dashboard → CI tokens)
|
|
253
256
|
steps:
|
|
254
257
|
- run: |
|
|
255
258
|
npx -y @andrian.yablonskyy/thub-agent run --type sw \
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andrian.yablonskyy/thub-agent",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
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.1.0",
|
|
16
16
|
"commander": "^13.1.0"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
package/src/cli.js
CHANGED
|
@@ -15,13 +15,13 @@
|
|
|
15
15
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
|
-
const { Command } = require('commander'),
|
|
18
|
+
const { Command, Option } = require('commander'),
|
|
19
19
|
{
|
|
20
20
|
ApiClient, EXIT_CODES, ACTIVE_JOB_STATES, exitCodeForJobState, PACKAGES, fetchLatestVersion, isNewer, isValidVersion, formatDateTime,
|
|
21
21
|
splitArgs,
|
|
22
22
|
parseEnvList
|
|
23
23
|
} = require('@andrian.yablonskyy/thub-common'),
|
|
24
|
-
{ resolveConnection, resolveGroup, resolveUser, writeConfigFile, readConfigFile } = require('./config'),
|
|
24
|
+
{ resolveConnection, resolveGroup, 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');
|
|
@@ -31,7 +31,8 @@ program
|
|
|
31
31
|
.name('thub')
|
|
32
32
|
.description('TestHub Agent — submit test jobs and follow them, from CI or your laptop')
|
|
33
33
|
.option('--url <url>', 'Coordinator URL (overrides THUB_URL / config file)')
|
|
34
|
-
.option('--
|
|
34
|
+
.option('--key <key>', 'Your access key (overrides THUB_KEY / config file)')
|
|
35
|
+
.addOption(new Option('--token <token>', 'Old name of --key').hideHelp())
|
|
35
36
|
.version(version);
|
|
36
37
|
|
|
37
38
|
// An admin-requested self-update (README §10.2) is applied at the start of
|
|
@@ -252,7 +253,7 @@ program
|
|
|
252
253
|
|
|
253
254
|
program
|
|
254
255
|
.command('status')
|
|
255
|
-
.description('Show status; follow log if running, verdict
|
|
256
|
+
.description('Show status; follow log if running, verdict, test counts and artifacts if done')
|
|
256
257
|
.argument('<jobId>')
|
|
257
258
|
.option('--json', 'Print the job as JSON once, without following it; exit code: its verdict, or 5 while it\'s still active', false)
|
|
258
259
|
.action(async (jobId, opts) => {
|
|
@@ -282,6 +283,13 @@ program
|
|
|
282
283
|
if (job.message){
|
|
283
284
|
console.log(`Message: ${job.message}`);
|
|
284
285
|
}
|
|
286
|
+
// What the job published elsewhere and reported (links, not files).
|
|
287
|
+
if (job.artifacts?.length){
|
|
288
|
+
console.log('Artifacts:');
|
|
289
|
+
for (const a of job.artifacts){
|
|
290
|
+
console.log(` ${a.name} ${a.size != null ? formatBytes(a.size) : '—'} ${a.link}`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
285
293
|
process.exit(exitCodeForJobState(job.state));
|
|
286
294
|
}
|
|
287
295
|
catch (err){
|
|
@@ -330,7 +338,7 @@ program
|
|
|
330
338
|
program
|
|
331
339
|
.command('jobs')
|
|
332
340
|
.description('List recent jobs')
|
|
333
|
-
.option('--mine', 'Only jobs submitted by this agent token', false)
|
|
341
|
+
.option('--mine', 'Only jobs submitted by this agent token (always so for a cli token, which sees only its own)', false)
|
|
334
342
|
.option('--state <state>', 'Filter by state')
|
|
335
343
|
.option('--json', 'Machine-readable output', false)
|
|
336
344
|
.action(async (opts) => {
|
|
@@ -352,19 +360,94 @@ program
|
|
|
352
360
|
}
|
|
353
361
|
});
|
|
354
362
|
|
|
363
|
+
// 1233 -> "1.2 KB" (1024-based), for artifact sizes.
|
|
364
|
+
function formatBytes(bytes){
|
|
365
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
|
366
|
+
let value = bytes,
|
|
367
|
+
unit = 0;
|
|
368
|
+
while (value >= 1024 && unit < units.length - 1){
|
|
369
|
+
value /= 1024;
|
|
370
|
+
unit += 1;
|
|
371
|
+
}
|
|
372
|
+
return unit === 0 ? `${value} B` : `${value < 10 ? value.toFixed(1) : Math.round(value)} ${units[unit]}`;
|
|
373
|
+
}
|
|
374
|
+
|
|
355
375
|
const config = program.command('config').description('Manage local Agent configuration');
|
|
356
376
|
config
|
|
357
377
|
.command('set')
|
|
358
|
-
.argument('<
|
|
378
|
+
.argument('<name>', 'url | key | group | user')
|
|
359
379
|
.argument('<value>')
|
|
360
|
-
.action((
|
|
361
|
-
|
|
362
|
-
|
|
380
|
+
.action((name, value) => {
|
|
381
|
+
// `token` is the old name of `key`.
|
|
382
|
+
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"');
|
|
363
385
|
process.exit(EXIT_CODES.USAGE);
|
|
364
386
|
}
|
|
365
|
-
const current = readConfigFile();
|
|
366
|
-
writeConfigFile({ ...current, [
|
|
367
|
-
console.log(`Saved ${
|
|
387
|
+
const { token: _old, ...current } = readConfigFile();
|
|
388
|
+
writeConfigFile({ ...(setting === 'key' ? current : { ...current, ...(_old ? { token: _old } : {}) }), [setting]: value });
|
|
389
|
+
console.log(`Saved ${setting} to config`);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// Who this access key belongs to (§10.3).
|
|
393
|
+
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}"`;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
program
|
|
399
|
+
.command('whoami')
|
|
400
|
+
.description('Show whose access key this Agent uses')
|
|
401
|
+
.action(async () => {
|
|
402
|
+
try {
|
|
403
|
+
console.log(describeMe(await client().get('/me')));
|
|
404
|
+
}
|
|
405
|
+
catch (err){
|
|
406
|
+
fail(err);
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
const keyCommand = program.command('key').description('Your access key');
|
|
411
|
+
keyCommand
|
|
412
|
+
.command('show')
|
|
413
|
+
.description('Show your access key\'s details (never the key itself — only its last characters)')
|
|
414
|
+
.action(async () => {
|
|
415
|
+
try {
|
|
416
|
+
const me = await client().get('/me'),
|
|
417
|
+
{ keySource } = resolveConnection(program.opts());
|
|
418
|
+
console.log(`User: ${describeMe(me)}`);
|
|
419
|
+
if (me.key){
|
|
420
|
+
console.log(`Key: …${me.key.hint || '????'} (from ${keySource === 'file' ? CONFIG_PATH : keySource === 'flag' ? '--key' : keySource})`);
|
|
421
|
+
console.log(`Created: ${formatDateTime(me.key.createdAt)}`);
|
|
422
|
+
console.log(`Last used: ${me.key.lastUsedAt ? formatDateTime(me.key.lastUsedAt) : 'never'}`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
catch (err){
|
|
426
|
+
fail(err);
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
keyCommand
|
|
430
|
+
.command('rotate')
|
|
431
|
+
.description('Replace your access key: the current one stops working at once')
|
|
432
|
+
.action(async () => {
|
|
433
|
+
try {
|
|
434
|
+
const { keySource } = resolveConnection(program.opts()),
|
|
435
|
+
{ key, username } = await client().post('/me/key/rotate');
|
|
436
|
+
if (keySource === 'file'){
|
|
437
|
+
const { token: _old, ...current } = readConfigFile();
|
|
438
|
+
writeConfigFile({ ...current, key });
|
|
439
|
+
console.log(`New access key for ${username} saved to ${CONFIG_PATH} — the old one has stopped working.`);
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
console.log(`New access key for ${username} — the old one has stopped working. Shown only now:\n\n ${key}\n`);
|
|
443
|
+
console.log(keySource === 'flag'
|
|
444
|
+
? 'Use it with --key from now on (or save it: thub config set key <key>).'
|
|
445
|
+
: `Update ${keySource} (e.g. your CI secret) with it, or save it: thub config set key <key>.`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
catch (err){
|
|
449
|
+
fail(err);
|
|
450
|
+
}
|
|
368
451
|
});
|
|
369
452
|
|
|
370
453
|
program
|
package/src/config.js
CHANGED
|
@@ -18,7 +18,8 @@ const fs = require('node:fs'),
|
|
|
18
18
|
path = require('node:path');
|
|
19
19
|
|
|
20
20
|
// §7: "Configuration is read from flags, then environment (THUB_URL,
|
|
21
|
-
//
|
|
21
|
+
// THUB_KEY), then ~/.config/thub/agent.json." The access key (§10.3) used to
|
|
22
|
+
// be called a token: --token, THUB_TOKEN and a saved `token` still work.
|
|
22
23
|
const CONFIG_PATH = path.join(os.homedir(), '.config', 'thub', 'agent.json'),
|
|
23
24
|
|
|
24
25
|
// Bundled with the package as a last-resort default, below the user's own
|
|
@@ -44,19 +45,24 @@ function writeConfigFile(config){
|
|
|
44
45
|
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 });
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
// `keySource` says where the key came from — `thub key rotate` saves the
|
|
49
|
+
// new one only where it can (the config file), and says what to update
|
|
50
|
+
// otherwise.
|
|
47
51
|
function resolveConnection(flags = {}){
|
|
48
52
|
const file = { ...readJsonFile(PACKAGE_DEFAULT_CONFIG_PATH), ...readConfigFile() },
|
|
49
53
|
url = flags.url || process.env.THUB_URL || file.url,
|
|
50
|
-
token = flags.
|
|
54
|
+
[token, keySource] = flags.key || flags.token ? [flags.key || flags.token, 'flag']
|
|
55
|
+
: process.env.THUB_KEY || process.env.THUB_TOKEN ? [process.env.THUB_KEY || process.env.THUB_TOKEN, process.env.THUB_KEY ? 'THUB_KEY' : 'THUB_TOKEN']
|
|
56
|
+
: file.key || file.token ? [file.key || file.token, 'file'] : [null, null];
|
|
51
57
|
if (!url || !token){
|
|
52
58
|
const err = new Error(
|
|
53
|
-
'Missing
|
|
54
|
-
'or THUB_URL /
|
|
59
|
+
'Missing Coordinator URL or access key. Set them with `thub config set url <url>` and `thub config set key <key>`, ' +
|
|
60
|
+
'or THUB_URL / THUB_KEY, or --url / --key. An admin gives you your key (Users), or create it on your dashboard profile.'
|
|
55
61
|
);
|
|
56
62
|
err.status = 4;
|
|
57
63
|
throw err;
|
|
58
64
|
}
|
|
59
|
-
return { url, token };
|
|
65
|
+
return { url, token, keySource };
|
|
60
66
|
}
|
|
61
67
|
|
|
62
68
|
// Optional default for `--group` (§4.3/§7.1) — same flags > env > file
|