@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.
Files changed (3) hide show
  1. package/README.md +36 -21
  2. package/package.json +1 -1
  3. 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 --image "$IMAGE_URL" --tests "$TESTS_URL" --wait
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
- | `--image <url>` | Firmware/build image URL, fetched by the Client. |
54
- | `--sha256 <hex>` | Expected sha256 of `--image`; the Client verifies it before flashing/running. |
55
- | `--tests <url>` / `--suite <name>` | Test package and suite. |
56
- | `--arg <value>` | Extra argument passed through to `run-tests.sh` on the Client (repeatable). |
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
- **Associate a CI/CD job id with the internal job id:**
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 sw --image "$IMAGE_URL" --tests "$TESTS_URL" \
85
- --meta ciJobId="$GITHUB_RUN_ID" --wait
86
- thub status A-00123 --json | jq '.spec.meta.ciJobId'
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
- **Pass Git repo/branch/hash/tag to the Client** — these travel as `--meta key=value` and the Client exposes each one to `run-tests.sh` as `THUB_META_<KEY>` (`ciJobId` → `THUB_META_CI_JOB_ID`):
94
+ **SW task in a Docker image of your own:**
90
95
 
91
96
  ```bash
92
- thub run --type hw --board nucleo-f401re \
93
- --image "$IMAGE_URL" --tests "$TESTS_URL" --suite smoke \
94
- --meta repo=yourorg/firmware --meta branch=main --meta sha=a1b2c3d --wait
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
- **Dry-run the pipeline** — proves the Coordinator↔Client plumbing works without real hardware, a real emulator image, or a reachable Artifactory:
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 --image https://does-not-exist.invalid/app.bin \
101
- --tests https://does-not-exist.invalid/tests.tar.gz --suite smoke \
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 --image "$IMAGE_URL" --tests "$TESTS_URL" \
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
- --image "$IMAGE_URL" --tests "$TESTS_URL" --user "Your Name" --wait
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
- --image "${{ needs.build.outputs.image_url }}" \
132
- --tests "${{ needs.build.outputs.tests_url }}" --suite full --wait
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andrian.yablonskyy/thub-agent",
3
- "version": "1.0.23",
3
+ "version": "1.0.24",
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"
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
- // `--image`: an http(s) URL is a firmware file to download; anything else
58
- // is taken as a Docker image reference, which only an SW job can run. Caught
59
- // here so `--type hw --image alpine` explains itself instead of failing
60
- // the Coordinator's schema check.
61
- function firmwareFromImage(opts){
62
- if (/^https?:\/\//i.test(opts.image)){
63
- return { url: opts.image, ...(opts.sha256 ? { sha256: opts.sha256 } : {}) };
64
- }
65
- if (/^[a-z][a-z0-9+.-]*:\/\//i.test(opts.image)){
66
- throw usageError(`--image ${opts.image}: only http(s) URLs can be downloaded as firmware`);
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
- // --tests (archive URL) or --tests-git (+ at most one of branch/tag/commit).
81
- function testsFromOptions(opts){
82
- const refs = ['branch', 'tag', 'commit'].filter((k) => opts[`tests${k[0].toUpperCase()}${k.slice(1)}`]);
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
- return { url: opts.tests };
73
+ task.image = opts.dockerImage;
94
74
  }
95
- if (refs.length > 1){
96
- throw usageError(`give at most one of --tests-branch, --tests-tag, --tests-commit (got ${refs.map((r) => `--tests-${r}`).join(', ')})`);
75
+
76
+ if (opts.depth !== undefined && !opts.gitRepo){
77
+ throw usageError('--depth only applies to --git-repo');
97
78
  }
98
- return {
99
- git: {
100
- url: opts.testsGit,
101
- ...(opts.testsBranch ? { branch: opts.testsBranch } : {}),
102
- ...(opts.testsTag ? { tag: opts.testsTag } : {}),
103
- ...(opts.testsCommit ? { commit: opts.testsCommit } : {})
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
- '--image <url|docker-image>',
140
- 'What the DUT runs: a firmware file URL (http/https, e.g. Artifactory) the Client downloads — HW and SW jobs — ' +
141
- 'or, for --type sw only, a Docker image to run as the DUT (e.g. alpine, alpine:3.20, registry.lab:5000/emu:1), ' +
142
- 'pulled from the Client\'s registry or Docker Hub; the Client must allow it (sw.allowJobImages)'
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
- '--tests <url>',
147
- 'Test sources as an archive URL (tar, tar.gz/.tgz/.bz2/.xz, or zip) the Client downloads and unpacks. ' +
148
- 'Give this or --tests-git'
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
- '--run <command>',
156
- 'Shell command that starts the tests, run in the test sources on the Client (--arg values arrive as "$@"). ' +
157
- 'Default: the sources\' own run-tests.sh. The Client must allow it (allowJobCommands)'
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('--suite <name>', 'Test suite name', 'default')
160
- .option('--arg <value>', 'Extra argument passed through to run-tests.sh / --run 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, [])
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 firmware = firmwareFromImage(opts),
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
- firmware,
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 } : {}),