@andrian.yablonskyy/thub-agent 1.0.20 → 1.0.22

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/cli.js +42 -8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andrian.yablonskyy/thub-agent",
3
- "version": "1.0.20",
3
+ "version": "1.0.22",
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"
package/src/cli.js CHANGED
@@ -16,8 +16,9 @@
16
16
  'use strict';
17
17
 
18
18
  const { Command } = require('commander'),
19
- { ApiClient, EXIT_CODES, ACTIVE_JOB_STATES, exitCodeForJobState, PACKAGES, fetchLatestVersion, isNewer, isValidVersion } =
20
- require('@andrian.yablonskyy/thub-common'),
19
+ {
20
+ ApiClient, EXIT_CODES, ACTIVE_JOB_STATES, exitCodeForJobState, PACKAGES, fetchLatestVersion, isNewer, isValidVersion, formatDateTime
21
+ } = require('@andrian.yablonskyy/thub-common'),
21
22
  { resolveConnection, resolveGroup, resolveUser, writeConfigFile, readConfigFile } = require('./config'),
22
23
  { parseDurationSec } = require('./duration'),
23
24
  { followJob } = require('./streaming'),
@@ -53,6 +54,33 @@ function client(){
53
54
  return new ApiClient({ baseUrl: url, token, userAgent: `thub-agent/${version}` });
54
55
  }
55
56
 
57
+ // `--image`: an http(s) URL is a firmware file to download; anything else
58
+ // is taken as a Docker image reference, which only an SW job can run. Caught
59
+ // here so `--type hw --image alpine` explains itself instead of failing
60
+ // the Coordinator's schema check.
61
+ function firmwareFromImage(opts){
62
+ if (/^https?:\/\//i.test(opts.image)){
63
+ return { url: opts.image, ...(opts.sha256 ? { sha256: opts.sha256 } : {}) };
64
+ }
65
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(opts.image)){
66
+ throw usageError(`--image ${opts.image}: only http(s) URLs can be downloaded as firmware`);
67
+ }
68
+ if (opts.type !== 'sw'){
69
+ throw usageError(
70
+ `--image ${opts.image} looks like a Docker image, which only an SW job can run (--type sw). ` +
71
+ 'An HW job flashes a firmware file: pass its http(s) URL.'
72
+ );
73
+ }
74
+ if (opts.sha256){
75
+ throw usageError('--sha256 applies to a firmware URL only; pin a Docker image by digest instead (image@sha256:...)');
76
+ }
77
+ return { image: opts.image };
78
+ }
79
+
80
+ function usageError(message){
81
+ return Object.assign(new Error(message), { status: EXIT_CODES.USAGE });
82
+ }
83
+
56
84
  function fail(err){
57
85
  console.error(`Error: ${err.message}`);
58
86
  process.exit(err.status && Number.isInteger(err.status) && err.status < 100 ? err.status : EXIT_CODES.USAGE);
@@ -79,8 +107,13 @@ program
79
107
  'Free-text job owner, shown on the Client and the dashboard to tell whose job is whose ' +
80
108
  '— purely a label, not an identity. Overrides THUB_USER / config file.'
81
109
  )
82
- .requiredOption('--image <url>', 'Firmware/build image URL (Artifactory or a Docker registry blob) fetched by the Client')
83
- .option('--sha256 <hex>', 'Expected sha256 of --image; the Client verifies it before flashing/running')
110
+ .requiredOption(
111
+ '--image <url|docker-image>',
112
+ 'What the DUT runs: a firmware file URL (http/https, e.g. Artifactory) the Client downloads — HW and SW jobs — ' +
113
+ 'or, for --type sw only, a Docker image to run as the DUT (e.g. alpine, alpine:3.20, registry.lab:5000/emu:1), ' +
114
+ 'pulled from the Client\'s registry or Docker Hub; the Client must allow it (sw.allowJobImages)'
115
+ )
116
+ .option('--sha256 <hex>', 'Expected sha256 of a firmware --image URL; the Client verifies it before flashing/running')
84
117
  .requiredOption('--tests <url>', 'Test package URL in Artifactory')
85
118
  .option('--suite <name>', 'Test suite name', 'default')
86
119
  .option('--arg <value>', 'Extra argument passed through to run-tests.sh on the Client (repeatable)', collectRepeatable, [])
@@ -104,7 +137,8 @@ program
104
137
  )
105
138
  .action(async (opts) => {
106
139
  try {
107
- const c = client(),
140
+ const firmware = firmwareFromImage(opts),
141
+ c = client(),
108
142
  labels = [...(opts.board ? [`board:${opts.board}`] : []), ...opts.label],
109
143
  meta = Object.fromEntries(opts.meta.map((kv) => kv.split(/=(.*)/s).slice(0, 2))),
110
144
  group = resolveGroup({ group: opts.group }),
@@ -112,7 +146,7 @@ program
112
146
 
113
147
  spec = {
114
148
  target: { type: opts.type, labels, ...(group ? { group } : {}), ...(opts.client ? { client: opts.client } : {}) },
115
- firmware: { url: opts.image, ...(opts.sha256 ? { sha256: opts.sha256 } : {}) },
149
+ firmware,
116
150
  tests: { url: opts.tests, suite: opts.suite, args: opts.arg },
117
151
  timeoutSec: parseDurationSec(opts.timeout),
118
152
  ...(opts.priority !== undefined ? { priority: opts.priority } : {}),
@@ -156,7 +190,7 @@ program
156
190
  }
157
191
 
158
192
  console.log(`Job ${job.id} — ${job.state}${job.resource ? ' on ' + job.resource.name : ''}`);
159
- console.log(`Created: ${job.created_at}`);
193
+ console.log(`Created: ${formatDateTime(job.created_at)}`);
160
194
 
161
195
  if (ACTIVE_JOB_STATES.has(job.state)){
162
196
  const code = await followJob(c, jobId, { fromSeq: 0, waitMode: false });
@@ -230,7 +264,7 @@ program
230
264
  ['SOURCE', (j) => j.source],
231
265
  ['USER', (j) => j.spec.user || '-'],
232
266
  ['STATE', (j) => j.state],
233
- ['CREATED', (j) => j.created_at]
267
+ ['CREATED', (j) => formatDateTime(j.created_at)]
234
268
  ]);
235
269
  }
236
270
  catch (err){