@andrian.yablonskyy/thub-common 1.0.23 → 1.0.24
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 +4 -3
- package/package.json +1 -1
- package/src/job-spec.schema.js +37 -44
- package/src/validate-job-spec.js +27 -45
package/README.md
CHANGED
|
@@ -21,12 +21,13 @@ const { validateJobSpec } = require('@andrian.yablonskyy/thub-common');
|
|
|
21
21
|
|
|
22
22
|
const { valid, spec, errors } = validateJobSpec({
|
|
23
23
|
target: { type: 'hw', labels: ['board:nucleo-f401re'] },
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
command: './ci/test.sh',
|
|
25
|
+
downloads: [{ url: 'https://artifactory.example.com/app.bin' }],
|
|
26
|
+
git: { url: 'https://github.com/yourorg/firmware-tests.git', ref: 'v1.4.0' }
|
|
26
27
|
});
|
|
27
28
|
```
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
A job is a task: `command` (required — the shell command the Client runs as its entry point, `args` as `"$@"`), with optional inputs `downloads` (`[{ url }]`), `git` (`url`, optional `ref` — branch, tag or commit — and `depth`, default 1) and, for SW jobs, `image` (a Docker image to run as the DUT). Other top-level fields: `target` (`type`, `labels`, optional `group`/`client`), `suite`, `timeoutSec`, `priority`, `source` (`ci`/`cli`), `user` (a free-text job-owner label), `meta` (arbitrary key/value metadata), and `dryRun`. A spec in the old shape (`firmware`/`tests`, from an Agent older than `--command`) is refused with a message to update the Agent.
|
|
30
31
|
|
|
31
32
|
### State enums and exit codes
|
|
32
33
|
|
package/package.json
CHANGED
package/src/job-spec.schema.js
CHANGED
|
@@ -18,19 +18,20 @@
|
|
|
18
18
|
const DOCKER_IMAGE_PATTERN =
|
|
19
19
|
'^[A-Za-z0-9][A-Za-z0-9.-]*(:[0-9]+)?(/[a-z0-9]+((\\.|_|__|-+)[a-z0-9]+)*)*(:[A-Za-z0-9_][A-Za-z0-9_.-]{0,127})?(@sha256:[a-f0-9]{64})?$',
|
|
20
20
|
|
|
21
|
-
// A git
|
|
22
|
-
//
|
|
21
|
+
// A git ref to check out — a branch, a tag or a commit (hex) — as git
|
|
22
|
+
// allows them: no leading "-" (never taken for an option) or "/", no "..",
|
|
23
|
+
// no spaces or the characters git forbids, not ending in "/", ".lock", ".".
|
|
23
24
|
GIT_REF_PATTERN = '^(?![-/])(?!.*\\.\\.)(?!.*//)(?!.*(/|\\.lock|\\.)$)[A-Za-z0-9._/+@-]+$',
|
|
24
25
|
|
|
25
|
-
// Matches the job spec shape documented in README.md §4.3.
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
26
|
+
// Matches the job spec shape documented in README.md §4.3. A task is:
|
|
27
|
+
// optionally files to download and/or a git checkout, optionally (SW only)
|
|
28
|
+
// a Docker image to run as the DUT, and — always — the shell command that
|
|
29
|
+
// is its entry point, run on the Client in the checkout / work directory.
|
|
29
30
|
jobSpecSchema = {
|
|
30
31
|
$id: 'https://thub.example.com/schemas/job-spec.json',
|
|
31
32
|
type: 'object',
|
|
32
33
|
additionalProperties: false,
|
|
33
|
-
required: ['target', '
|
|
34
|
+
required: ['target', 'command'],
|
|
34
35
|
properties: {
|
|
35
36
|
target: {
|
|
36
37
|
type: 'object',
|
|
@@ -56,47 +57,39 @@ const DOCKER_IMAGE_PATTERN =
|
|
|
56
57
|
client: { type: 'string', minLength: 1 }
|
|
57
58
|
}
|
|
58
59
|
},
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
60
|
+
// The task's entry point (`thub run --command`): a shell command the
|
|
61
|
+
// Client runs with `sh -c`, `args` as "$@" (`--arg`, repeatable).
|
|
62
|
+
command: { type: 'string', minLength: 1, maxLength: 4096 },
|
|
63
|
+
args: { type: 'array', items: { type: 'string' }, default: [] },
|
|
64
|
+
// Passed to the command as THUB_SUITE (`--suite`).
|
|
65
|
+
suite: { type: 'string', default: 'default' },
|
|
66
|
+
// Files the Client downloads into the task's work directory before
|
|
67
|
+
// running the command (`--download-file`, repeatable).
|
|
68
|
+
downloads: {
|
|
69
|
+
type: 'array',
|
|
70
|
+
maxItems: 32,
|
|
71
|
+
items: {
|
|
72
|
+
type: 'object',
|
|
73
|
+
additionalProperties: false,
|
|
74
|
+
required: ['url'],
|
|
75
|
+
properties: { url: { type: 'string', format: 'uri' } }
|
|
76
|
+
},
|
|
77
|
+
default: []
|
|
72
78
|
},
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
|
|
79
|
+
// A Docker image an SW Client runs as the DUT instead of its own
|
|
80
|
+
// sw.image, if it allows that (sw.allowJobImages) — `--docker-image`.
|
|
81
|
+
image: { type: 'string', maxLength: 255, pattern: DOCKER_IMAGE_PATTERN },
|
|
82
|
+
// A repository the Client clones before running the command — at `ref`
|
|
83
|
+
// (branch, tag or commit; default: the default branch), `depth` commits
|
|
84
|
+
// deep (0 = full history). `--git-repo <url> [ref] [--depth <n>]`.
|
|
85
|
+
git: {
|
|
80
86
|
type: 'object',
|
|
81
87
|
additionalProperties: false,
|
|
88
|
+
required: ['url'],
|
|
82
89
|
properties: {
|
|
83
|
-
url: { type: 'string',
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
additionalProperties: false,
|
|
87
|
-
required: ['url'],
|
|
88
|
-
properties: {
|
|
89
|
-
url: { type: 'string', minLength: 1, maxLength: 2048 },
|
|
90
|
-
// Ref names as git allows them — never starting with "-",
|
|
91
|
-
// so none can be taken for a git option.
|
|
92
|
-
branch: { type: 'string', maxLength: 255, pattern: GIT_REF_PATTERN },
|
|
93
|
-
tag: { type: 'string', maxLength: 255, pattern: GIT_REF_PATTERN },
|
|
94
|
-
commit: { type: 'string', pattern: '^[0-9a-fA-F]{7,40}$' }
|
|
95
|
-
}
|
|
96
|
-
},
|
|
97
|
-
command: { type: 'string', minLength: 1, maxLength: 4096 },
|
|
98
|
-
suite: { type: 'string', default: 'default' },
|
|
99
|
-
args: { type: 'array', items: { type: 'string' }, default: [] }
|
|
90
|
+
url: { type: 'string', minLength: 1, maxLength: 2048 },
|
|
91
|
+
ref: { type: 'string', maxLength: 255, pattern: GIT_REF_PATTERN },
|
|
92
|
+
depth: { type: 'integer', minimum: 0, maximum: 100000, default: 1 }
|
|
100
93
|
}
|
|
101
94
|
},
|
|
102
95
|
timeoutSec: { type: 'integer', minimum: 1, default: 1800 },
|
package/src/validate-job-spec.js
CHANGED
|
@@ -26,62 +26,44 @@ const validateFn = ajv.compile(jobSpecSchema);
|
|
|
26
26
|
* Returns { valid, spec, errors }.
|
|
27
27
|
*/
|
|
28
28
|
function validateJobSpec(spec){
|
|
29
|
+
const legacy = legacyShapeError(spec);
|
|
30
|
+
if (legacy){
|
|
31
|
+
return { valid: false, spec: JSON.parse(JSON.stringify(spec ?? {})), errors: [legacy] };
|
|
32
|
+
}
|
|
29
33
|
const clone = JSON.parse(JSON.stringify(spec ?? {})),
|
|
30
34
|
schemaValid = validateFn(clone),
|
|
31
35
|
errors = schemaValid ? [] : (validateFn.errors || []).map((e) => `${e.instancePath || '/'} ${e.message}`);
|
|
32
|
-
errors.push(...
|
|
36
|
+
errors.push(...crossFieldErrors(clone));
|
|
33
37
|
return { valid: errors.length === 0, spec: clone, errors };
|
|
34
38
|
}
|
|
35
39
|
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const tests = spec.tests;
|
|
44
|
-
if (!tests || typeof tests !== 'object'){
|
|
45
|
-
return [];
|
|
46
|
-
}
|
|
47
|
-
if (tests.url && tests.git){
|
|
48
|
-
return ['/tests give either url (an archive) or git (a repository), not both'];
|
|
49
|
-
}
|
|
50
|
-
if (!tests.url && !tests.git){
|
|
51
|
-
return ['/tests needs url (an archive: tar/tar.gz/zip) or git (a repository)'];
|
|
52
|
-
}
|
|
53
|
-
if (tests.git){
|
|
54
|
-
if (!GIT_URL.test(tests.git.url || '')){
|
|
55
|
-
return ['/tests/git/url must be an https://, http://, ssh:// or git:// URL, or user@host:path'];
|
|
56
|
-
}
|
|
57
|
-
const refs = ['branch', 'tag', 'commit'].filter((k) => tests.git[k]);
|
|
58
|
-
if (refs.length > 1){
|
|
59
|
-
return [`/tests/git give at most one of branch, tag or commit (got ${refs.join(', ')})`];
|
|
60
|
-
}
|
|
40
|
+
// Specs from an Agent older than --command (firmware/tests fields) can't be
|
|
41
|
+
// translated faithfully (they relied on the Client flashing and on
|
|
42
|
+
// run-tests.sh) — say what to do instead of listing unknown fields.
|
|
43
|
+
function legacyShapeError(spec){
|
|
44
|
+
if (spec && typeof spec === 'object' && ('firmware' in spec || 'tests' in spec) && !('command' in spec)){
|
|
45
|
+
return '/ this job spec is from an older Agent (firmware/tests fields) — update the Agent (thub self-update) ' +
|
|
46
|
+
'and use --command, --download-file, --docker-image and --git-repo';
|
|
61
47
|
}
|
|
62
|
-
return
|
|
48
|
+
return null;
|
|
63
49
|
}
|
|
64
50
|
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
return ['/firmware needs url (a firmware file) or image (a Docker image, SW jobs only)'];
|
|
77
|
-
}
|
|
78
|
-
if (fw.image && spec.target?.type === 'hw'){
|
|
79
|
-
return ['/firmware/image a Docker image only works for SW jobs (target.type "sw") — an HW job flashes a firmware file: use firmware.url'];
|
|
51
|
+
// Git transports a Client may fetch sources over. Never `ext::` (runs a
|
|
52
|
+
// command), `file://` or a local path: the Client also sets
|
|
53
|
+
// GIT_ALLOW_PROTOCOL to the same list (downloader.js).
|
|
54
|
+
const GIT_URL = /^(?:(?:https?|ssh|git):\/\/[^\s]+|[A-Za-z0-9._-]+@[A-Za-z0-9.-]+:[^\s]+)$/;
|
|
55
|
+
|
|
56
|
+
// Rules across fields, spelled out here rather than as schema if/then,
|
|
57
|
+
// whose errors ("must match a schema in then") say little.
|
|
58
|
+
function crossFieldErrors(spec){
|
|
59
|
+
const errors = [];
|
|
60
|
+
if (spec.image && spec.target?.type === 'hw'){
|
|
61
|
+
errors.push('/image a Docker image only works for SW jobs (target.type "sw")');
|
|
80
62
|
}
|
|
81
|
-
if (
|
|
82
|
-
|
|
63
|
+
if (spec.git && !GIT_URL.test(spec.git.url || '')){
|
|
64
|
+
errors.push('/git/url must be an https://, http://, ssh:// or git:// URL, or user@host:path');
|
|
83
65
|
}
|
|
84
|
-
return
|
|
66
|
+
return errors;
|
|
85
67
|
}
|
|
86
68
|
|
|
87
69
|
module.exports = { validateJobSpec };
|