@andrian.yablonskyy/thub-agent 1.0.22 → 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 +56 -29
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,27 +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
|
-
|
|
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 } : {}) };
|
|
68
|
+
|
|
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');
|
|
72
|
+
}
|
|
73
|
+
task.image = opts.dockerImage;
|
|
67
74
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
'An HW job flashes a firmware file: pass its http(s) URL.'
|
|
72
|
-
);
|
|
75
|
+
|
|
76
|
+
if (opts.depth !== undefined && !opts.gitRepo){
|
|
77
|
+
throw usageError('--depth only applies to --git-repo');
|
|
73
78
|
}
|
|
74
|
-
if (opts.
|
|
75
|
-
|
|
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)`);
|
|
87
|
+
}
|
|
88
|
+
task.git = { url, ...(ref ? { ref } : {}), depth };
|
|
76
89
|
}
|
|
77
|
-
return
|
|
90
|
+
return task;
|
|
78
91
|
}
|
|
79
92
|
|
|
80
93
|
function usageError(message){
|
|
@@ -108,15 +121,30 @@ program
|
|
|
108
121
|
'— purely a label, not an identity. Overrides THUB_USER / config file.'
|
|
109
122
|
)
|
|
110
123
|
.requiredOption(
|
|
111
|
-
'--
|
|
112
|
-
'
|
|
113
|
-
'
|
|
114
|
-
|
|
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
|
+
[]
|
|
134
|
+
)
|
|
135
|
+
.option(
|
|
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)'
|
|
139
|
+
)
|
|
140
|
+
.option(
|
|
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)'
|
|
115
144
|
)
|
|
116
|
-
.option('--
|
|
117
|
-
.
|
|
118
|
-
.option('--
|
|
119
|
-
.option('--arg <value>', 'Extra argument passed through to run-tests.sh on the Client (repeatable)', collectRepeatable, [])
|
|
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, [])
|
|
120
148
|
.option('--timeout <duration>', 'e.g. 30m, 1h', '30m')
|
|
121
149
|
.option('--priority <n>', 'Priority 0-100', (v) => Number(v))
|
|
122
150
|
.option('--wait', 'Do not detach on job end; exit with the verdict code (used in CI)', false)
|
|
@@ -137,7 +165,7 @@ program
|
|
|
137
165
|
)
|
|
138
166
|
.action(async (opts) => {
|
|
139
167
|
try {
|
|
140
|
-
const
|
|
168
|
+
const task = taskFromOptions(opts),
|
|
141
169
|
c = client(),
|
|
142
170
|
labels = [...(opts.board ? [`board:${opts.board}`] : []), ...opts.label],
|
|
143
171
|
meta = Object.fromEntries(opts.meta.map((kv) => kv.split(/=(.*)/s).slice(0, 2))),
|
|
@@ -146,8 +174,7 @@ program
|
|
|
146
174
|
|
|
147
175
|
spec = {
|
|
148
176
|
target: { type: opts.type, labels, ...(group ? { group } : {}), ...(opts.client ? { client: opts.client } : {}) },
|
|
149
|
-
|
|
150
|
-
tests: { url: opts.tests, suite: opts.suite, args: opts.arg },
|
|
177
|
+
...task,
|
|
151
178
|
timeoutSec: parseDurationSec(opts.timeout),
|
|
152
179
|
...(opts.priority !== undefined ? { priority: opts.priority } : {}),
|
|
153
180
|
...(user ? { user } : {}),
|