@andrian.yablonskyy/thub-agent 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 +36 -21
- package/package.json +1 -1
- package/src/cli.js +50 -65
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ See the [main TestHub repo](https://github.com/andrianyablonskyy/thub) for the f
|
|
|
9
9
|
```bash
|
|
10
10
|
npm i -g @andrian.yablonskyy/thub-agent
|
|
11
11
|
# or, one-off in CI:
|
|
12
|
-
npx -y @andrian.yablonskyy/thub-agent run --type sw --
|
|
12
|
+
npx -y @andrian.yablonskyy/thub-agent run --type sw --download-file "$IMAGE_URL" --git-repo "$TESTS_REPO" --command ./ci/test.sh --wait
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
## Configuration
|
|
@@ -50,10 +50,13 @@ Key options for `thub run`:
|
|
|
50
50
|
| `--group <groupId>` | Restrict scheduling to resources that are members of this group. Falls back to `THUB_GROUP` / `thub config set group <id>`. |
|
|
51
51
|
| `--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. |
|
|
52
52
|
| `--user <name>` | Free-text job owner — a label, not an identity. Falls back to `THUB_USER` / `thub config set user <name>`. |
|
|
53
|
-
| `--
|
|
54
|
-
| `--
|
|
55
|
-
| `--
|
|
56
|
-
| `--
|
|
53
|
+
| `--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_*`. |
|
|
54
|
+
| `--download-file <url>` | A file the Client downloads before running the command (repeatable, `http(s)`), into the job's `downloads/` directory. |
|
|
55
|
+
| `--docker-image <name>` | SW only: a Docker image the Client runs as the DUT instead of its own `sw.image` (the Client must allow it: `sw.allowJobImages`). |
|
|
56
|
+
| `--git-repo <url> [<branch>\|<tag>\|<commit>]` | A git repository the Client clones (default ref: the default branch); the command runs in the checkout. |
|
|
57
|
+
| `--depth <n>` | With `--git-repo`: commits to fetch, default `1`; `0` = full history. |
|
|
58
|
+
| `--suite <name>` | Passed to the command as `THUB_SUITE`. |
|
|
59
|
+
| `--arg <value>` | Extra argument for the command, as `"$@"` (repeatable). |
|
|
57
60
|
| `--timeout <dur>` | e.g. `30m`, default `30m`. |
|
|
58
61
|
| `--priority <n>` | 0–100; CI defaults to 50, CLI to 60 so a developer is not starved by a busy pipeline. |
|
|
59
62
|
| `--meta <key=value>` | Arbitrary metadata stored on the job (repeatable) — CI job ids, git coordinates, anything else worth attaching to the run. |
|
|
@@ -78,34 +81,45 @@ Exit codes make the Agent usable as a CI step:
|
|
|
78
81
|
|
|
79
82
|
## Examples
|
|
80
83
|
|
|
81
|
-
**
|
|
84
|
+
**Run a task** — download the firmware, check out the tests at a tag, flash and test (HW):
|
|
82
85
|
|
|
83
86
|
```bash
|
|
84
|
-
thub run --type
|
|
85
|
-
--
|
|
86
|
-
|
|
87
|
+
thub run --type hw --board nucleo-f401re \
|
|
88
|
+
--download-file "$IMAGE_URL" \
|
|
89
|
+
--git-repo https://github.com/yourorg/firmware-tests.git v1.4.0 \
|
|
90
|
+
--command 'st-flash --serial "$THUB_DUT_STLINK" --reset write "$THUB_DOWNLOAD_1" 0x08000000 && ./ci/test.sh "$@"' \
|
|
91
|
+
--arg --junit --wait
|
|
87
92
|
```
|
|
88
93
|
|
|
89
|
-
**
|
|
94
|
+
**SW task in a Docker image of your own:**
|
|
90
95
|
|
|
91
96
|
```bash
|
|
92
|
-
thub run --type
|
|
93
|
-
--
|
|
94
|
-
--
|
|
97
|
+
thub run --type sw --docker-image alpine:3.20 \
|
|
98
|
+
--git-repo git@github.com:yourorg/firmware-tests.git main --depth 20 \
|
|
99
|
+
--command 'make test' --wait
|
|
95
100
|
```
|
|
96
101
|
|
|
97
|
-
**
|
|
102
|
+
**Associate a CI/CD job id with the internal job id:**
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
thub run --type sw --download-file "$IMAGE_URL" --git-repo "$TESTS_REPO" --command ./ci/test.sh \
|
|
106
|
+
--meta ciJobId="$GITHUB_RUN_ID" --wait
|
|
107
|
+
thub status A-00123 --json | jq '.spec.meta.ciJobId'
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Pass extra metadata** — `--meta key=value` reaches the command as `THUB_META_<KEY>` (`ciJobId` → `THUB_META_CI_JOB_ID`).
|
|
111
|
+
|
|
112
|
+
**Dry-run the pipeline** — proves the Coordinator↔Client plumbing works without real hardware, a real emulator image, or reachable downloads:
|
|
98
113
|
|
|
99
114
|
```bash
|
|
100
|
-
thub run --type sw --
|
|
101
|
-
--
|
|
102
|
-
--dry-run --wait
|
|
115
|
+
thub run --type sw --download-file https://does-not-exist.invalid/app.bin \
|
|
116
|
+
--command ./ci/test.sh --dry-run --wait
|
|
103
117
|
```
|
|
104
118
|
|
|
105
119
|
**Run on a specific resource group only:**
|
|
106
120
|
|
|
107
121
|
```bash
|
|
108
|
-
thub run --type sw --
|
|
122
|
+
thub run --type sw --git-repo "$TESTS_REPO" --command ./ci/test.sh \
|
|
109
123
|
--group 548ae4ae-ac5b-401f-acaa-24bbe790e62d --wait
|
|
110
124
|
```
|
|
111
125
|
|
|
@@ -113,7 +127,7 @@ thub run --type sw --image "$IMAGE_URL" --tests "$TESTS_URL" \
|
|
|
113
127
|
|
|
114
128
|
```bash
|
|
115
129
|
thub run --type hw --board nucleo-f401re \
|
|
116
|
-
--
|
|
130
|
+
--git-repo "$TESTS_REPO" --command ./ci/test.sh --user "Your Name" --wait
|
|
117
131
|
```
|
|
118
132
|
|
|
119
133
|
## GitHub Actions
|
|
@@ -128,8 +142,9 @@ test-sw:
|
|
|
128
142
|
steps:
|
|
129
143
|
- run: |
|
|
130
144
|
npx -y @andrian.yablonskyy/thub-agent run --type sw \
|
|
131
|
-
--
|
|
132
|
-
--
|
|
145
|
+
--download-file "${{ needs.build.outputs.image_url }}" \
|
|
146
|
+
--git-repo "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY.git" "$GITHUB_SHA" \
|
|
147
|
+
--command ./ci/sw-tests.sh --suite full --wait
|
|
133
148
|
```
|
|
134
149
|
|
|
135
150
|
If the GitHub job is canceled, the runner sends `SIGINT` to the Agent. In `--wait` mode (CI), that's treated as a cancel request (`POST /jobs/:id/cancel`) before exiting, so abandoned CI jobs don't hold hardware. In interactive mode, Ctrl-C only detaches.
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -54,55 +54,40 @@ function client(){
|
|
|
54
54
|
return new ApiClient({ baseUrl: url, token, userAgent: `thub-agent/${version}` });
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
// here so
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
}
|
|
57
|
+
// --command (mandatory), --download-file (repeatable), --docker-image (SW
|
|
58
|
+
// only) and --git-repo <url> [ref] --depth <n> -> the job spec's task fields.
|
|
59
|
+
// Checked here too, so mistakes explain themselves before anything is sent.
|
|
60
|
+
function taskFromOptions(opts){
|
|
61
|
+
const downloads = opts.downloadFile.map((url) => {
|
|
62
|
+
if (!/^https?:\/\//i.test(url)){
|
|
63
|
+
throw usageError(`--download-file ${url}: must be an http(s) URL`);
|
|
64
|
+
}
|
|
65
|
+
return { url };
|
|
66
|
+
}),
|
|
67
|
+
task = { command: opts.command, args: opts.arg, suite: opts.suite, ...(downloads.length ? { downloads } : {}) };
|
|
79
68
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (opts.tests && opts.testsGit){
|
|
84
|
-
throw usageError('give either --tests (an archive URL) or --tests-git (a repository), not both');
|
|
85
|
-
}
|
|
86
|
-
if (!opts.tests && !opts.testsGit){
|
|
87
|
-
throw usageError('missing test sources: --tests <archive-url> or --tests-git <repo> [--tests-branch|--tests-tag|--tests-commit]');
|
|
88
|
-
}
|
|
89
|
-
if (opts.tests){
|
|
90
|
-
if (refs.length){
|
|
91
|
-
throw usageError(`--tests-${refs[0]} only applies to --tests-git`);
|
|
69
|
+
if (opts.dockerImage){
|
|
70
|
+
if (opts.type !== 'sw'){
|
|
71
|
+
throw usageError('--docker-image only works for SW jobs (--type sw): an HW job runs on the physical board');
|
|
92
72
|
}
|
|
93
|
-
|
|
73
|
+
task.image = opts.dockerImage;
|
|
94
74
|
}
|
|
95
|
-
|
|
96
|
-
|
|
75
|
+
|
|
76
|
+
if (opts.depth !== undefined && !opts.gitRepo){
|
|
77
|
+
throw usageError('--depth only applies to --git-repo');
|
|
97
78
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
79
|
+
if (opts.gitRepo){
|
|
80
|
+
const [url, ref, ...extra] = opts.gitRepo;
|
|
81
|
+
if (extra.length){
|
|
82
|
+
throw usageError(`--git-repo takes a URL and at most one branch, tag or commit (got: ${opts.gitRepo.join(' ')})`);
|
|
83
|
+
}
|
|
84
|
+
const depth = opts.depth === undefined ? 1 : Number(opts.depth);
|
|
85
|
+
if (!Number.isInteger(depth) || depth < 0){
|
|
86
|
+
throw usageError(`--depth ${opts.depth}: must be a whole number (0 = full history)`);
|
|
104
87
|
}
|
|
105
|
-
|
|
88
|
+
task.git = { url, ...(ref ? { ref } : {}), depth };
|
|
89
|
+
}
|
|
90
|
+
return task;
|
|
106
91
|
}
|
|
107
92
|
|
|
108
93
|
function usageError(message){
|
|
@@ -136,28 +121,30 @@ program
|
|
|
136
121
|
'— purely a label, not an identity. Overrides THUB_USER / config file.'
|
|
137
122
|
)
|
|
138
123
|
.requiredOption(
|
|
139
|
-
'--
|
|
140
|
-
'
|
|
141
|
-
'
|
|
142
|
-
|
|
124
|
+
'--command <string>',
|
|
125
|
+
'The task\'s entry point: a shell command the Client runs (sh -c) in the task\'s work directory — the --git-repo ' +
|
|
126
|
+
'checkout if given — after downloading --download-file files. --arg values arrive as "$@"'
|
|
127
|
+
)
|
|
128
|
+
.option(
|
|
129
|
+
'--download-file <url>',
|
|
130
|
+
'A file the Client downloads into the work directory before running --command (repeatable; http/https). ' +
|
|
131
|
+
'Paths are passed as THUB_DOWNLOAD_1.. / THUB_DOWNLOADS_DIR',
|
|
132
|
+
collectRepeatable,
|
|
133
|
+
[]
|
|
143
134
|
)
|
|
144
|
-
.option('--sha256 <hex>', 'Expected sha256 of a firmware --image URL; the Client verifies it before flashing/running')
|
|
145
135
|
.option(
|
|
146
|
-
'--
|
|
147
|
-
'
|
|
148
|
-
'
|
|
136
|
+
'--docker-image <name>',
|
|
137
|
+
'SW jobs only: a Docker image the Client runs as the DUT (e.g. alpine, alpine:3.20, registry.lab:5000/emu:1), ' +
|
|
138
|
+
'instead of its own sw.image; pulled from its registry or Docker Hub. The Client must allow it (sw.allowJobImages)'
|
|
149
139
|
)
|
|
150
|
-
.option('--tests-git <repo>', 'Test sources as a git repository (https://, ssh://, git:// or user@host:path) the Client fetches')
|
|
151
|
-
.option('--tests-branch <name>', 'With --tests-git: the branch to check out (default: the repository\'s default branch)')
|
|
152
|
-
.option('--tests-tag <name>', 'With --tests-git: the tag to check out')
|
|
153
|
-
.option('--tests-commit <sha>', 'With --tests-git: the commit to check out (7-40 hex digits)')
|
|
154
140
|
.option(
|
|
155
|
-
'--
|
|
156
|
-
'
|
|
157
|
-
'
|
|
141
|
+
'--git-repo <url...>',
|
|
142
|
+
'A git repository the Client clones before running --command, then runs it there: <url> [<branch>|<tag>|<commit>] ' +
|
|
143
|
+
'(https://, ssh://, git:// or user@host:path; default ref: the default branch)'
|
|
158
144
|
)
|
|
159
|
-
.option('--
|
|
160
|
-
.option('--
|
|
145
|
+
.option('--depth <n>', 'With --git-repo: how many commits to fetch (default 1; 0 = full history)')
|
|
146
|
+
.option('--suite <name>', 'Test suite name, passed to --command as THUB_SUITE', 'default')
|
|
147
|
+
.option('--arg <value>', 'Extra argument for --command, as "$@" (repeatable)', collectRepeatable, [])
|
|
161
148
|
.option('--timeout <duration>', 'e.g. 30m, 1h', '30m')
|
|
162
149
|
.option('--priority <n>', 'Priority 0-100', (v) => Number(v))
|
|
163
150
|
.option('--wait', 'Do not detach on job end; exit with the verdict code (used in CI)', false)
|
|
@@ -178,8 +165,7 @@ program
|
|
|
178
165
|
)
|
|
179
166
|
.action(async (opts) => {
|
|
180
167
|
try {
|
|
181
|
-
const
|
|
182
|
-
testSources = testsFromOptions(opts),
|
|
168
|
+
const task = taskFromOptions(opts),
|
|
183
169
|
c = client(),
|
|
184
170
|
labels = [...(opts.board ? [`board:${opts.board}`] : []), ...opts.label],
|
|
185
171
|
meta = Object.fromEntries(opts.meta.map((kv) => kv.split(/=(.*)/s).slice(0, 2))),
|
|
@@ -188,8 +174,7 @@ program
|
|
|
188
174
|
|
|
189
175
|
spec = {
|
|
190
176
|
target: { type: opts.type, labels, ...(group ? { group } : {}), ...(opts.client ? { client: opts.client } : {}) },
|
|
191
|
-
|
|
192
|
-
tests: { ...testSources, ...(opts.run ? { command: opts.run } : {}), suite: opts.suite, args: opts.arg },
|
|
177
|
+
...task,
|
|
193
178
|
timeoutSec: parseDurationSec(opts.timeout),
|
|
194
179
|
...(opts.priority !== undefined ? { priority: opts.priority } : {}),
|
|
195
180
|
...(user ? { user } : {}),
|