@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 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" --git-repo "$TESTS_REPO" --command ./ci/test.sh --wait
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`, `THUB_USER`; 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.
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
- | `--board <name>` / `--label <l>` | Required labels (repeatable). | `JOB_BOARD`; `JOB_LABEL`, `JOB_LABEL_<n>` |
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
- | `--user <name>` | Free-text job owner — a label, not an identity. Falls back to `THUB_USER` / `thub config set user <name>`. | `JOB_USER` |
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, check out the tests at a tag, flash and test (HW):
82
+ **Run a task** — download the firmware, clone the tests at a tag, flash and test (HW):
89
83
 
90
84
  ```bash
91
- thub run --type hw --board nucleo-f401re \
92
- --download-file "$IMAGE_URL" \
93
- --git-repo https://github.com/yourorg/firmware-tests.git v1.4.0 \
94
- --command 'st-flash --serial "$THUB_DUT_STLINK" --reset write "$THUB_DOWNLOAD_1" 0x08000000 && ./ci/test.sh "$@"' \
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 a DUT emulator image of your own** (the command runs on the Client host and talks to the emulator):
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 --docker-image registry.lab.local:5000/dut-emulator:2026.08 \
102
- --git-repo git@github.com:yourorg/firmware-tests.git main --depth 20 \
103
- --command 'make test DUT="$THUB_DUT_HOST"' --wait
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" --git-repo "$TESTS_REPO" --command ./ci/test.sh \
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, a real emulator image, or reachable downloads:
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
- **Label a job with its owner** — purely informational, shows up on the dashboard, in `thub jobs`, and on the Client's own console:
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 every command the Client runs for the job: git and `--command`. 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:
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 --git-repo "$TESTS_REPO" \
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`, `--git-options` — is stored as is and visible, so never put secrets there.
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
- **Pull an image as the DUT:** `--docker-image` is pulled by the Client before the command runs, as its service user. For a private registry, log in once on the Client host as that user: `sudo -u thub docker login registry.lab.local:5000`.
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-repo git@bitbucket.org:yourorg/web-ui-tests.git main \
179
- --git-options '-c core.sshCommand="ssh -i /home/thub/.ssh/id_ed25519 -o StrictHostKeyChecking=accept-new"' \
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; SW Clients have both. 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.
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), `THUB_GIT_COMMIT`, `THUB_DOWNLOADS_DIR`, `THUB_DOWNLOADS`, `THUB_DOWNLOAD_<n>` (local paths), `THUB_SUITE` and `THUB_META_<KEY>`;
209
- - the DUT's `THUB_DUT_UART[_<n>]`, `THUB_DUT_USB[_<n>]`, `THUB_DUT_STLINK[_<n>]` (HW), or `THUB_DUT_HOST` and `THUB_DUT_CONTAINER` (SW with `--docker-image`).
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 sw --git-repo git@bitbucket.org:yourorg/tests.git main --depth 1 \
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 --git-repo "$TESTS_REPO" --command ./ci/test.sh --detach --json | jq -r .jobId)
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
- --git-repo "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY.git" "$GITHUB_SHA" \
254
- --command ./ci/sw-tests.sh --suite full --wait
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.2",
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.2",
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, resolveUser, writeConfigFile, readConfigFile, CONFIG_PATH } = require('./config'),
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), --docker-image (SW
61
- // only) and --git-repo <url> [ref] --depth <n> -> the job spec's task fields.
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('--board <name>', 'Shorthand for --label board:<name>')
142
- .option('--label <label>', 'Required label the resource must have (repeatable)', collectRepeatable, [])
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 task\'s work directory — the --git-repo ' +
156
- 'checkout if given — after downloading --download-file files. --arg values arrive as "$@"'
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 every command the Client runs for the job (git, --command): ' +
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 (except THUB_*, JOB_*, GIT_TERMINAL_PROMPT, GIT_ALLOW_PROTOCOL). ' +
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 = [...(opts.board ? [`board:${opts.board}`] : []), ...opts.label],
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 uses this key's, set on the dashboard (§13.1).
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 | user')
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', 'user'].includes(setting)){
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
- : 'Error: the setting must be "url", "key", or "user"');
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
- // Optional default for `--user` (§4.3/§7.1) — flags > env > file, like
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 };