@andrian.yablonskyy/thub-agent 1.1.2 → 1.1.4
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 +47 -51
- package/package.json +2 -2
- package/src/cli.js +33 -70
- package/src/config.js +1 -10
package/README.md
CHANGED
|
@@ -9,12 +9,12 @@ 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 --download-file "$IMAGE_URL" --
|
|
12
|
+
npx -y @andrian.yablonskyy/thub-agent run --type sw --download-file "$IMAGE_URL" --command ./ci/test.sh --wait
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
## Configuration
|
|
16
16
|
|
|
17
|
-
Read from flags (`--url`, `--key`), then environment (`THUB_URL`, `THUB_KEY
|
|
17
|
+
Read from flags (`--url`, `--key`), then environment (`THUB_URL`, `THUB_KEY`; 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
|
|
|
@@ -24,7 +24,6 @@ thub config set key thk_... # your access key: from an admin (Users), or y
|
|
|
24
24
|
thub whoami # jane (user) <jane@example.com>
|
|
25
25
|
thub key show # its last characters, created, last used
|
|
26
26
|
thub key rotate # a new key; the old one stops at once (saved here if it came from here)
|
|
27
|
-
thub config set user "Your Name" # optional default --user
|
|
28
27
|
```
|
|
29
28
|
|
|
30
29
|
## Commands
|
|
@@ -48,15 +47,10 @@ Key options for `thub run`. **On the Client** names the environment variable the
|
|
|
48
47
|
| Option | Description | On the Client |
|
|
49
48
|
|---|---|---|
|
|
50
49
|
| `--type hw\|sw` | Required resource type. | `JOB_TYPE` |
|
|
51
|
-
| `--
|
|
50
|
+
| `--label <l>` | Required labels (repeatable; a board is `--label board:<name>`): the job runs only on a runner that has all of them. Each 1–64 characters, no whitespace, commas or semicolons. | `JOB_LABEL`, `JOB_LABEL_<n>` |
|
|
52
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. | `JOB_CLIENT` |
|
|
53
|
-
| `--
|
|
54
|
-
| `--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_*`. | `JOB_COMMAND` |
|
|
52
|
+
| `--command <string>` | **Required.** The task's entry point: a shell command the Client runs (`sh -c`) in the job's work directory, after the downloads. It clones repositories and runs containers itself (see Git and Docker below), with credentials from `--env`. Exit code 0 = PASSED. | `JOB_COMMAND` | `JOB_COMMAND` |
|
|
55
53
|
| `--download-file <url>` | A file the Client downloads before running the command (repeatable, `http(s)`), into the job's `downloads/` directory. | `JOB_DOWNLOAD_FILE`, `JOB_DOWNLOAD_FILE_<n>` (URLs); `THUB_DOWNLOAD_<n>` (local paths) |
|
|
56
|
-
| `--docker-image <name>` | SW only: a Docker image the Client runs as the **DUT** (an emulator the tests talk to, at `$THUB_DUT_HOST` / `$THUB_DUT_CONTAINER`) — any SW Client runs it; without one, an SW job has no DUT container. It's not where `--command` runs: to run the tests in an image, see [Docker](#docker). | `JOB_DOCKER_IMAGE` |
|
|
57
|
-
| `--git-repo <url> [<branch>\|<tag>\|<commit>]` | A git repository the Client clones (default ref: the default branch); the command runs in the checkout. | `JOB_GIT_REPO_URL`, `JOB_GIT_BRANCH` |
|
|
58
|
-
| `--depth <n>` | With `--git-repo`: commits to fetch, default `1`; `0` = full history. | `JOB_GIT_DEPTH` |
|
|
59
|
-
| `--git-options <string>` | With `--git-repo`: extra git options placed between `git` and its subcommand on the Client, e.g. `'-c core.sshCommand="ssh -i ~/.ssh/lab_key -p 2222"'`. Shell-quoted (no shell run). Stored with the job, so reference key files rather than inlining secrets. | `JOB_GIT_OPTIONS` |
|
|
60
54
|
| `--env <vars>` | Environment variables for every command the Client runs for the job (git and `--command`): `NAME=value[,NAME=value]`, repeatable; `--env NAME` alone takes the value from your shell. Any names — none means anything to the Agent or the Client (only `THUB_*`, `JOB_*`, `GIT_TERMINAL_PROMPT`, `GIT_ALLOW_PROTOCOL` are refused). Every value is a secret: it reaches only the Client running the job; the Coordinator masks it and drops it when the job ends. | each `NAME` itself |
|
|
61
55
|
| `--suite <name>` | Passed to the command as `THUB_SUITE`. | `JOB_SUITE` (also `THUB_SUITE`) |
|
|
62
56
|
| `--arg <value>` | Extra argument for the command, as `"$@"` (repeatable). | `JOB_ARG`, `JOB_ARG_<n>` (and `"$@"`) |
|
|
@@ -85,35 +79,37 @@ Exit codes make the Agent usable as a CI step:
|
|
|
85
79
|
|
|
86
80
|
## Examples
|
|
87
81
|
|
|
88
|
-
**Run a task** — download the firmware,
|
|
82
|
+
**Run a task** — download the firmware, clone the tests at a tag, flash and test (HW):
|
|
89
83
|
|
|
90
84
|
```bash
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
--
|
|
94
|
-
--command '
|
|
85
|
+
export GH_TOKEN=… # read access to the tests repository
|
|
86
|
+
thub run --type hw --label board:nucleo-f401re \
|
|
87
|
+
--download-file "$IMAGE_URL" --env GH_TOKEN \
|
|
88
|
+
--command 'git clone --depth 1 --branch v1.4.0 "https://x-access-token:$GH_TOKEN@github.com/yourorg/firmware-tests.git" . &&
|
|
89
|
+
st-flash --serial "$THUB_DUT_STLINK" --reset write "$THUB_DOWNLOAD_1" 0x08000000 && ./ci/test.sh "$@"' \
|
|
95
90
|
--arg --junit --wait
|
|
96
91
|
```
|
|
97
92
|
|
|
98
|
-
**SW task against
|
|
93
|
+
**SW task against an emulator container of your own** (started and removed by the command):
|
|
99
94
|
|
|
100
95
|
```bash
|
|
101
|
-
thub run --type sw --
|
|
102
|
-
--
|
|
103
|
-
|
|
96
|
+
thub run --type sw --download-file "$IMAGE_URL" \
|
|
97
|
+
--command 'dut="thub-$THUB_JOB_ID" && trap "docker rm -f $dut >/dev/null" EXIT &&
|
|
98
|
+
docker run -d --name "$dut" -p 127.0.0.1:5555:5555 -v "$THUB_DOWNLOADS_DIR:/downloads:ro" registry.lab.local:5000/dut-emulator:2026.08 &&
|
|
99
|
+
make test DUT=127.0.0.1:5555' --wait
|
|
104
100
|
```
|
|
105
101
|
|
|
106
102
|
**Associate a CI/CD job id with the internal job id:**
|
|
107
103
|
|
|
108
104
|
```bash
|
|
109
|
-
thub run --type sw --download-file "$IMAGE_URL" --
|
|
105
|
+
thub run --type sw --download-file "$IMAGE_URL" --command ./ci/test.sh \
|
|
110
106
|
--meta ciJobId="$GITHUB_RUN_ID" --wait
|
|
111
107
|
thub status A-00123 --json | jq '.spec.meta.ciJobId'
|
|
112
108
|
```
|
|
113
109
|
|
|
114
110
|
**Pass extra metadata** — `--meta key=value` reaches the command as `THUB_META_<KEY>` (`ciJobId` → `THUB_META_CI_JOB_ID`).
|
|
115
111
|
|
|
116
|
-
**Dry-run the pipeline** — proves the Coordinator↔Client plumbing works without real hardware
|
|
112
|
+
**Dry-run the pipeline** — proves the Coordinator↔Client plumbing works without real hardware or reachable downloads:
|
|
117
113
|
|
|
118
114
|
```bash
|
|
119
115
|
thub run --type sw --download-file https://does-not-exist.invalid/app.bin \
|
|
@@ -122,20 +118,15 @@ thub run --type sw --download-file https://does-not-exist.invalid/app.bin \
|
|
|
122
118
|
|
|
123
119
|
**Run in a resource group:** there's no `--group` option. An admin or maintainer gives your user (or a CI token) one group on the dashboard, and every job it submits runs only on that group's resources; `thub whoami` shows it.
|
|
124
120
|
|
|
125
|
-
**
|
|
126
|
-
|
|
127
|
-
```bash
|
|
128
|
-
thub run --type hw --board nucleo-f401re \
|
|
129
|
-
--git-repo "$TESTS_REPO" --command ./ci/test.sh --user "Your Name" --wait
|
|
130
|
-
```
|
|
121
|
+
**Who submitted a job** comes from your key: your username (a CI token: its name), shown on the dashboard, in `thub jobs`, on the Client's console and as `JOB_USER`. There's no `--user`.
|
|
131
122
|
|
|
132
123
|
## Environment variables and secrets
|
|
133
124
|
|
|
134
|
-
`--env NAME=value[,NAME=value]` (repeatable) sets variables for
|
|
125
|
+
`--env NAME=value[,NAME=value]` (repeatable) sets variables for `--command`. It's how all data and secrets reach a job (a git token, a registry password, an API key). The names are yours; none means anything to the Agent or the Client. `--env NAME` alone takes the value from your own environment, so a secret stays off the command line and out of CI logs:
|
|
135
126
|
|
|
136
127
|
```bash
|
|
137
128
|
export API_TOKEN=…
|
|
138
|
-
thub run --type sw
|
|
129
|
+
thub run --type sw \
|
|
139
130
|
--env TARGET=staging --env API_TOKEN \
|
|
140
131
|
--command './run-tests.sh --target "$TARGET"' --wait
|
|
141
132
|
```
|
|
@@ -147,10 +138,24 @@ thub run --type sw --git-repo "$TESTS_REPO" \
|
|
|
147
138
|
- The Coordinator replaces the values with `***` once the job ends.
|
|
148
139
|
- A `--dry-run` shows every value as `***`, whatever its name.
|
|
149
140
|
|
|
150
|
-
Your command's output is the job's log, so don't print secrets. Everything else — `--command`, `--arg`, `--meta`, `--
|
|
141
|
+
Your command's output is the job's log, so don't print secrets. Everything else — `--command`, `--arg`, `--meta`, `--download-file` — is stored as is and visible, so never put secrets there.
|
|
142
|
+
|
|
143
|
+
## Git
|
|
144
|
+
|
|
145
|
+
The Client doesn't clone anything (and doesn't need git itself): the command does, into its work directory, with a token from `--env`:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
export GH_TOKEN=…
|
|
149
|
+
thub run --type sw --env GH_TOKEN,REF=main \
|
|
150
|
+
--command 'git clone --depth 1 --branch "$REF" "https://x-access-token:$GH_TOKEN@github.com/yourorg/tests.git" . && ./ci/test.sh' --wait
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
For SSH, pass the private key itself as `--env` (base64) and use it via `GIT_SSH_COMMAND` from a file in the job directory, which is deleted with the job (main README, §7.2).
|
|
151
154
|
|
|
152
155
|
## Docker
|
|
153
156
|
|
|
157
|
+
The Client doesn't pull images or start containers (and doesn't need Docker itself): the command does.
|
|
158
|
+
|
|
154
159
|
**Log in to a custom registry:** in `--command`, with credentials passed as `--env` under any names. Point `DOCKER_CONFIG` at the job's work directory first, so the login is deleted with the job rather than left in the Client user's `~/.docker` for later jobs:
|
|
155
160
|
|
|
156
161
|
```bash
|
|
@@ -163,21 +168,13 @@ thub run --type hw \
|
|
|
163
168
|
--wait
|
|
164
169
|
```
|
|
165
170
|
|
|
166
|
-
**
|
|
167
|
-
|
|
168
|
-
```bash
|
|
169
|
-
thub run --type sw --docker-image registry.lab.local:5000/dut-emulator:2026.08 \
|
|
170
|
-
--git-repo "$TESTS_REPO" --command './ci/test.sh --dut "$THUB_DUT_HOST"' --wait
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
**Run the command inside a container:** `--command` starts on the Client host in the work directory (`$THUB_WORK_DIR`, the `--git-repo` checkout). Start the container from it, mounting that directory:
|
|
171
|
+
**Run the command inside a container:** `--command` starts on the Client host in the work directory (`$THUB_WORK_DIR`). Clone into it, then start the container mounting it:
|
|
174
172
|
|
|
175
173
|
```bash
|
|
176
174
|
thub run --type sw \
|
|
177
|
-
--env DOCKER_REGISTRY=registry.lab.local:5000,DOCKER_USER=ci --env DOCKER_PASSWORD \
|
|
178
|
-
--git-
|
|
179
|
-
|
|
180
|
-
--command 'export DOCKER_CONFIG="$THUB_WORK_DIR/.docker" &&
|
|
175
|
+
--env DOCKER_REGISTRY=registry.lab.local:5000,DOCKER_USER=ci --env DOCKER_PASSWORD --env GH_TOKEN \
|
|
176
|
+
--command 'git clone --depth 1 "https://x-access-token:$GH_TOKEN@github.com/yourorg/web-ui-tests.git" . &&
|
|
177
|
+
export DOCKER_CONFIG="$THUB_WORK_DIR/.docker" &&
|
|
181
178
|
echo "$DOCKER_PASSWORD" | docker login "$DOCKER_REGISTRY" --username "$DOCKER_USER" --password-stdin &&
|
|
182
179
|
docker run --rm -v "$THUB_WORK_DIR:/work" -w /work "$DOCKER_REGISTRY/python:3.14" ./run-tests.sh' \
|
|
183
180
|
--wait
|
|
@@ -199,21 +196,19 @@ thub run --type sw \
|
|
|
199
196
|
--wait
|
|
200
197
|
```
|
|
201
198
|
|
|
202
|
-
The Client host needs Docker and the Client's user in the `docker` group
|
|
199
|
+
The Client host needs Docker and the Client's user in the `docker` group for these (install Docker before the Client). Add `--dry-run` to see every command a job would run on the Client (every `--env` value shown as `***`) without running any. More in the main README, §7.2.
|
|
203
200
|
|
|
204
201
|
## Client environment variables
|
|
205
202
|
|
|
206
203
|
On the Client, the job's `--command` gets every option above as a `JOB_<NAME>` variable (the **On the Client** column). A variable whose option wasn't given is unset. A repeatable option gives `<NAME>` with all values plus `<NAME>_<n>` for each one. It also gets:
|
|
207
204
|
- its `--env` variables, under their own names;
|
|
208
|
-
- `THUB_JOB_ID`, `THUB_WORK_DIR` (where it runs), `
|
|
209
|
-
- the DUT's `THUB_DUT_UART[_<n>]`, `THUB_DUT_USB[_<n>]`, `THUB_DUT_STLINK[_<n>]` (HW)
|
|
205
|
+
- `THUB_JOB_ID`, `THUB_WORK_DIR` (where it runs), `THUB_DOWNLOADS_DIR`, `THUB_DOWNLOADS`, `THUB_DOWNLOAD_<n>` (local paths), `THUB_SUITE` and `THUB_META_<KEY>`;
|
|
206
|
+
- the DUT's `THUB_DUT_UART[_<n>]`, `THUB_DUT_USB[_<n>]`, `THUB_DUT_STLINK[_<n>]` (HW).
|
|
210
207
|
|
|
211
208
|
`THUB_*` and `JOB_*` names can't be set with `--env`. The full list is in the main README, §7.4.
|
|
212
209
|
|
|
213
210
|
```bash
|
|
214
|
-
thub run --type
|
|
215
|
-
--command 'git clone --depth "$JOB_GIT_DEPTH" ${JOB_GIT_BRANCH:+--branch "$JOB_GIT_BRANCH"} "$JOB_GIT_REPO_URL" src && cd src && ./run-tests.sh' \
|
|
216
|
-
--wait
|
|
211
|
+
thub run --type hw --label uart --suite smoke --command 'echo "suite $JOB_SUITE on $JOB_LABEL" && ./ci/test.sh' --wait
|
|
217
212
|
```
|
|
218
213
|
|
|
219
214
|
## Job status and PASS/FAIL
|
|
@@ -232,7 +227,7 @@ thub jobs --mine --state FAILED
|
|
|
232
227
|
**From a script:** `thub status <jobId> --json` prints the job once, with `state`, `exit_code`, `summary`, `message` and `resource.name`. It exits with the verdict code, or `5` while the job is still queued or running:
|
|
233
228
|
|
|
234
229
|
```bash
|
|
235
|
-
JOB=$(thub run --type sw --
|
|
230
|
+
JOB=$(thub run --type sw --command ./ci/test.sh --detach --json | jq -r .jobId)
|
|
236
231
|
while thub status "$JOB" --json > job.json; [ $? -eq 5 ]; do sleep 10; done
|
|
237
232
|
jq -r '"\(.state) exit=\(.exit_code) failed=\(.summary.failed // 0)"' job.json # PASSED exit=0 failed=0
|
|
238
233
|
```
|
|
@@ -250,8 +245,9 @@ test-sw:
|
|
|
250
245
|
- run: |
|
|
251
246
|
npx -y @andrian.yablonskyy/thub-agent run --type sw \
|
|
252
247
|
--download-file "${{ needs.build.outputs.image_url }}" \
|
|
253
|
-
--
|
|
254
|
-
--command
|
|
248
|
+
--env GH_TOKEN="${{ secrets.TESTS_READ_TOKEN }}",REPO="$GITHUB_REPOSITORY",SHA="$GITHUB_SHA" \
|
|
249
|
+
--command 'git init -q . && git fetch -q --depth 1 "https://x-access-token:$GH_TOKEN@github.com/$REPO.git" "$SHA" &&
|
|
250
|
+
git checkout -q FETCH_HEAD && ./ci/sw-tests.sh' --suite full --wait
|
|
255
251
|
```
|
|
256
252
|
|
|
257
253
|
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andrian.yablonskyy/thub-agent",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
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.1.
|
|
15
|
+
"@andrian.yablonskyy/thub-common": "^1.1.3",
|
|
16
16
|
"commander": "^13.1.0"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
package/src/cli.js
CHANGED
|
@@ -18,10 +18,9 @@
|
|
|
18
18
|
const { Command, Option } = require('commander'),
|
|
19
19
|
{
|
|
20
20
|
ApiClient, EXIT_CODES, ACTIVE_JOB_STATES, exitCodeForJobState, PACKAGES, fetchLatestVersion, isNewer, isValidVersion, formatDateTime,
|
|
21
|
-
splitArgs,
|
|
22
21
|
parseEnvList
|
|
23
22
|
} = require('@andrian.yablonskyy/thub-common'),
|
|
24
|
-
{ resolveConnection,
|
|
23
|
+
{ resolveConnection, writeConfigFile, readConfigFile, CONFIG_PATH } = require('./config'),
|
|
25
24
|
{ parseDurationSec } = require('./duration'),
|
|
26
25
|
{ followJob } = require('./streaming'),
|
|
27
26
|
{ applyRequestedUpdate, installAgent, version } = require('./self-update');
|
|
@@ -57,8 +56,8 @@ function client(){
|
|
|
57
56
|
return new ApiClient({ baseUrl: url, token, userAgent: `thub-agent/${version}` });
|
|
58
57
|
}
|
|
59
58
|
|
|
60
|
-
// --command (mandatory), --download-file (repeatable)
|
|
61
|
-
//
|
|
59
|
+
// --command (mandatory), --download-file (repeatable) and --env -> the job
|
|
60
|
+
// spec's task fields. A git checkout or a container is the command's own job.
|
|
62
61
|
// Checked here too, so mistakes explain themselves before anything is sent.
|
|
63
62
|
function taskFromOptions(opts){
|
|
64
63
|
const downloads = opts.downloadFile.map((url) => {
|
|
@@ -69,38 +68,6 @@ function taskFromOptions(opts){
|
|
|
69
68
|
}),
|
|
70
69
|
task = { command: opts.command, args: opts.arg, suite: opts.suite, ...(downloads.length ? { downloads } : {}) };
|
|
71
70
|
|
|
72
|
-
if (opts.dockerImage){
|
|
73
|
-
if (opts.type !== 'sw'){
|
|
74
|
-
throw usageError('--docker-image only works for SW jobs (--type sw): an HW job runs on the physical board');
|
|
75
|
-
}
|
|
76
|
-
task.image = opts.dockerImage;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (opts.depth !== undefined && !opts.gitRepo){
|
|
80
|
-
throw usageError('--depth only applies to --git-repo');
|
|
81
|
-
}
|
|
82
|
-
if (opts.gitOptions !== undefined && !opts.gitRepo){
|
|
83
|
-
throw usageError('--git-options only applies to --git-repo');
|
|
84
|
-
}
|
|
85
|
-
if (opts.gitOptions){
|
|
86
|
-
try {
|
|
87
|
-
splitArgs(opts.gitOptions);
|
|
88
|
-
}
|
|
89
|
-
catch (err){
|
|
90
|
-
throw usageError(`--git-options: ${err.message}`);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
if (opts.gitRepo){
|
|
94
|
-
const [url, ref, ...extra] = opts.gitRepo;
|
|
95
|
-
if (extra.length){
|
|
96
|
-
throw usageError(`--git-repo takes a URL and at most one branch, tag or commit (got: ${opts.gitRepo.join(' ')})`);
|
|
97
|
-
}
|
|
98
|
-
const depth = opts.depth === undefined ? 1 : Number(opts.depth);
|
|
99
|
-
if (!Number.isInteger(depth) || depth < 0){
|
|
100
|
-
throw usageError(`--depth ${opts.depth}: must be a whole number (0 = full history)`);
|
|
101
|
-
}
|
|
102
|
-
task.git = { url, ...(ref ? { ref } : {}), depth, ...(opts.gitOptions ? { options: opts.gitOptions } : {}) };
|
|
103
|
-
}
|
|
104
71
|
let env;
|
|
105
72
|
try {
|
|
106
73
|
env = parseEnvList(opts.env);
|
|
@@ -134,26 +101,38 @@ function fail(err){
|
|
|
134
101
|
process.exit(err.status && Number.isInteger(err.status) && err.status < 100 ? err.status : EXIT_CODES.USAGE);
|
|
135
102
|
}
|
|
136
103
|
|
|
104
|
+
// --label: the labels a runner must all have for the job (§7.1).
|
|
105
|
+
// Checked here against the Coordinator's rule, so a typo fails before the
|
|
106
|
+
// job is sent.
|
|
107
|
+
const LABEL_RE = /^[^\s,;]{1,64}$/;
|
|
108
|
+
function requiredLabels(opts){
|
|
109
|
+
const labels = opts.label,
|
|
110
|
+
bad = labels.filter((l) => !LABEL_RE.test(l));
|
|
111
|
+
if (bad.length){
|
|
112
|
+
const err = new Error(`Invalid label: ${bad.join(', ')} — 1–64 characters, no spaces, commas or semicolons`);
|
|
113
|
+
err.status = EXIT_CODES.USAGE;
|
|
114
|
+
throw err;
|
|
115
|
+
}
|
|
116
|
+
return [...new Set(labels)];
|
|
117
|
+
}
|
|
118
|
+
|
|
137
119
|
program
|
|
138
120
|
.command('run')
|
|
139
121
|
.description('Submit a test job and follow its log')
|
|
140
122
|
.requiredOption('--type <hw|sw>', 'Required resource type')
|
|
141
|
-
.option('--
|
|
142
|
-
|
|
123
|
+
.option('--label <label>', 'A label the runner must have (repeatable; the job runs only on a runner with all of them). ' +
|
|
124
|
+
'1–64 characters, no spaces, commas or semicolons', collectRepeatable, [])
|
|
143
125
|
.option(
|
|
144
126
|
'--client <nameOrId>',
|
|
145
127
|
'Run on this specific Client (resource name or id) only; the job waits in that Client\'s queue ' +
|
|
146
128
|
'even if other matching resources are idle.'
|
|
147
129
|
)
|
|
148
|
-
.option(
|
|
149
|
-
'--user <name>',
|
|
150
|
-
'Free-text job owner, shown on the Client and the dashboard to tell whose job is whose ' +
|
|
151
|
-
'— purely a label, not an identity. Overrides THUB_USER / config file.'
|
|
152
|
-
)
|
|
153
130
|
.requiredOption(
|
|
154
131
|
'--command <string>',
|
|
155
|
-
'The task\'s entry point: a shell command the Client runs (sh -c) in the
|
|
156
|
-
'
|
|
132
|
+
'The task\'s entry point: a shell command the Client runs (sh -c) in the job\'s work directory, after downloading ' +
|
|
133
|
+
'--download-file files. --arg values arrive as "$@". Anything else the job needs it does itself — e.g. ' +
|
|
134
|
+
'`git clone "https://x-access-token:$GH_TOKEN@github.com/org/tests.git" . && ./ci/test.sh` or ' +
|
|
135
|
+
'`docker run --rm "$IMAGE" ./run.sh` — with credentials passed by --env'
|
|
157
136
|
)
|
|
158
137
|
.option(
|
|
159
138
|
'--download-file <url>',
|
|
@@ -162,27 +141,11 @@ program
|
|
|
162
141
|
collectRepeatable,
|
|
163
142
|
[]
|
|
164
143
|
)
|
|
165
|
-
.option(
|
|
166
|
-
'--docker-image <name>',
|
|
167
|
-
'SW jobs only: a Docker image the Client runs as the job\'s DUT container, next to --command (e.g. registry.lab:5000/emu:1); ' +
|
|
168
|
-
'pulled from the registry it names, else Docker Hub. Without it, an SW job has no DUT container'
|
|
169
|
-
)
|
|
170
|
-
.option(
|
|
171
|
-
'--git-repo <url...>',
|
|
172
|
-
'A git repository the Client clones before running --command, then runs it there: <url> [<branch>|<tag>|<commit>] ' +
|
|
173
|
-
'(https://, ssh://, git:// or user@host:path; default ref: the default branch)'
|
|
174
|
-
)
|
|
175
|
-
.option('--depth <n>', 'With --git-repo: how many commits to fetch (default 1; 0 = full history)')
|
|
176
|
-
.option(
|
|
177
|
-
'--git-options <string>',
|
|
178
|
-
'With --git-repo: extra git options, placed between `git` and its subcommand on the Client (shell-quoted, no shell run), ' +
|
|
179
|
-
'e.g. \'-c core.sshCommand="ssh -i ~/.ssh/lab_key -p 2222"\'. Stored with the job — reference key files, don\'t inline secrets'
|
|
180
|
-
)
|
|
181
144
|
.option(
|
|
182
145
|
'--env <vars>',
|
|
183
|
-
'Environment variables for
|
|
146
|
+
'Environment variables for --command — how data and secrets (a git token, a registry password) reach the job: ' +
|
|
184
147
|
'NAME=value[,NAME=value] (repeatable; a value may contain commas); --env NAME alone takes its value from this shell. ' +
|
|
185
|
-
'Any names
|
|
148
|
+
'Any names except THUB_* and JOB_*. ' +
|
|
186
149
|
'Values reach only the Client running the job; the Coordinator masks them and drops them when the job ends',
|
|
187
150
|
collectRepeatable,
|
|
188
151
|
[]
|
|
@@ -211,17 +174,15 @@ program
|
|
|
211
174
|
try {
|
|
212
175
|
const task = taskFromOptions(opts),
|
|
213
176
|
c = client(),
|
|
214
|
-
labels =
|
|
177
|
+
labels = requiredLabels(opts),
|
|
215
178
|
meta = Object.fromEntries(opts.meta.map((kv) => kv.split(/=(.*)/s).slice(0, 2))),
|
|
216
|
-
user = resolveUser({ user: opts.user }),
|
|
217
179
|
|
|
218
180
|
spec = {
|
|
219
|
-
// No group: the Coordinator
|
|
181
|
+
// No group or user: the Coordinator takes both from this key (§13.1).
|
|
220
182
|
target: { type: opts.type, labels, ...(opts.client ? { client: opts.client } : {}) },
|
|
221
183
|
...task,
|
|
222
184
|
timeoutSec: parseDurationSec(opts.timeout),
|
|
223
185
|
...(opts.priority !== undefined ? { priority: opts.priority } : {}),
|
|
224
|
-
...(user ? { user } : {}),
|
|
225
186
|
...(Object.keys(meta).length ? { meta } : {}),
|
|
226
187
|
...(opts.dryRun ? { dryRun: true } : {})
|
|
227
188
|
},
|
|
@@ -370,15 +331,17 @@ function formatBytes(bytes){
|
|
|
370
331
|
const config = program.command('config').description('Manage local Agent configuration');
|
|
371
332
|
config
|
|
372
333
|
.command('set')
|
|
373
|
-
.argument('<name>', 'url | key
|
|
334
|
+
.argument('<name>', 'url | key')
|
|
374
335
|
.argument('<value>')
|
|
375
336
|
.action((name, value) => {
|
|
376
337
|
// `token` is the old name of `key`.
|
|
377
338
|
const setting = name === 'token' ? 'key' : name;
|
|
378
|
-
if (!['url', 'key'
|
|
339
|
+
if (!['url', 'key'].includes(setting)){
|
|
379
340
|
console.error(setting === 'group'
|
|
380
341
|
? 'Error: a job\'s group is set on the dashboard now (Users / CI tokens), not in the Agent'
|
|
381
|
-
:
|
|
342
|
+
: setting === 'user'
|
|
343
|
+
? 'Error: a job\'s user comes from your access key now (thub whoami shows it), not from the Agent'
|
|
344
|
+
: 'Error: the setting must be "url" or "key"');
|
|
382
345
|
process.exit(EXIT_CODES.USAGE);
|
|
383
346
|
}
|
|
384
347
|
const { token: _old, ...current } = readConfigFile();
|
package/src/config.js
CHANGED
|
@@ -65,13 +65,4 @@ function resolveConnection(flags = {}){
|
|
|
65
65
|
return { url, token, keySource };
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
// url/token. Purely a label (who submitted this job), not an
|
|
70
|
-
// identity: unset is fine, and nothing on the Coordinator side enforces
|
|
71
|
-
// or authenticates it.
|
|
72
|
-
function resolveUser(flags = {}){
|
|
73
|
-
const file = { ...readJsonFile(PACKAGE_DEFAULT_CONFIG_PATH), ...readConfigFile() };
|
|
74
|
-
return flags.user || process.env.THUB_USER || file.user || undefined;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
module.exports = { CONFIG_PATH, readConfigFile, writeConfigFile, resolveConnection, resolveUser };
|
|
68
|
+
module.exports = { CONFIG_PATH, readConfigFile, writeConfigFile, resolveConnection };
|