@andrian.yablonskyy/thub-coordinator 1.0.36 → 1.0.38

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 (44) hide show
  1. package/package.json +2 -2
  2. package/public/css/thub.css +129 -0
  3. package/public/js/config-import.js +37 -33
  4. package/public/js/copy-to-clipboard.js +21 -18
  5. package/public/js/help.js +93 -0
  6. package/public/js/list-search.js +51 -0
  7. package/public/js/live.js +287 -0
  8. package/public/js/log-viewer.js +123 -11
  9. package/public/js/remove-resource.js +5 -3
  10. package/public/js/resource-card.js +14 -13
  11. package/public/js/tooltips.js +3 -1
  12. package/src/server.js +20 -2
  13. package/src/services/jobs.js +29 -12
  14. package/src/services/list-prefs.js +4 -0
  15. package/src/services/live.js +124 -0
  16. package/src/services/search.js +59 -0
  17. package/src/web/routes.js +130 -21
  18. package/test/list-prefs.test.js +6 -0
  19. package/test/live.test.js +153 -0
  20. package/test/search.test.js +97 -0
  21. package/test/views.test.js +30 -0
  22. package/views/admin/agents.pug +28 -10
  23. package/views/groups/list.pug +14 -1
  24. package/views/help/_agent-cli.pug +103 -0
  25. package/views/help/_agent-setup.pug +72 -0
  26. package/views/help/_ci.pug +44 -0
  27. package/views/help/_client-machines.pug +87 -0
  28. package/views/help/_client-setup.pug +122 -0
  29. package/views/help/_coordinator.pug +134 -0
  30. package/views/help/_docker.pug +124 -0
  31. package/views/help/_env.pug +127 -0
  32. package/views/help/_git.pug +129 -0
  33. package/views/help/_mixins.pug +24 -0
  34. package/views/help/_overview.pug +118 -0
  35. package/views/help/_quick-start.pug +37 -0
  36. package/views/help/_troubleshooting.pug +42 -0
  37. package/views/help/index.pug +67 -0
  38. package/views/index.pug +3 -2
  39. package/views/jobs/list.pug +6 -3
  40. package/views/layout.pug +24 -1
  41. package/views/mixins/list-controls.pug +36 -0
  42. package/views/mixins/log-viewer.pug +25 -3
  43. package/views/mixins/resource.pug +1 -1
  44. package/views/resources/list.pug +7 -4
@@ -0,0 +1,122 @@
1
+ +section('client-setup', 'Client setup', 'motherboard')
2
+ p.
3
+ A Client (#[code thub-client]) is a daemon on a lab machine. One #[strong instance] owns one DUT slot. A machine with
4
+ several boards runs several instances (#[code dut0], #[code dut1], …), each with its own config file.
5
+ Clients register themselves with the shared join key; there's no admin step.
6
+
7
+ h3.h6 1. Install
8
+ +code('Lab machine, as the user the Client should run as (via sudo)').
9
+ # Node.js and HW tools (stlink-tools/openocd only for HW Clients)
10
+ sudo apt install -y nodejs npm stlink-tools openocd usbutils
11
+ # Docker: needed on SW Clients, and on HW Clients whose jobs use docker.
12
+ # Install it BEFORE thub-client so the service gets the docker group.
13
+ sudo apt install -y docker.io
14
+ sudo npm i -g @andrian.yablonskyy/thub-client
15
+ p.small.
16
+ This creates #[code ~/.config/thub/client.json] and #[code ~/var/lib/thub/client] for the user who ran
17
+ #[code sudo], and installs the #[code thub-client@.service] systemd unit running as that user, with the
18
+ #[code dialout], #[code plugdev] and #[code docker] groups. It also installs the root helpers that
19
+ apply self-updates and scheduled reboots.
20
+
21
+ h3.h6 2. Configure and start
22
+ .row.g-3
23
+ .col-md-6
24
+ +code('SW Client — ~/.config/thub/client.json').
25
+ {
26
+ "coordinatorUrl": "#{coordinatorUrl}",
27
+ "name": "lab-sw-01",
28
+ "type": "sw",
29
+ "labels": [],
30
+ "groups": [],
31
+ "joinKey": "<the Coordinator's clientJoinKey>",
32
+ "varDir": "/home/thub/var/lib/thub/client"
33
+ }
34
+ .col-md-6
35
+ +code('HW Client — ~/.config/thub/client.json').
36
+ {
37
+ "coordinatorUrl": "#{coordinatorUrl}",
38
+ "name": "lab-hw-01",
39
+ "type": "hw",
40
+ "labels": ["board:nucleo-f401re", "uart", "stlink"],
41
+ "groups": ["<group id>"],
42
+ "joinKey": "<the Coordinator's clientJoinKey>",
43
+ "varDir": "/home/thub/var/lib/thub/client",
44
+ "hw-devices": {
45
+ "stlinks": [{ "index": 1, "devpath": "3.3.4.3.1" }],
46
+ "uarts": [{ "index": 1, "devpath": "3.3.3.2", "baudRate": 115200 }],
47
+ "usbs": [{ "index": 1, "devpath": "3.2.4" }]
48
+ }
49
+ }
50
+ +code('Start it and follow the log').
51
+ sudo systemctl enable --now thub-client@client
52
+ journalctl -u thub-client@client -f
53
+
54
+ h3.h6 Config reference
55
+ .table-responsive
56
+ table.table.table-sm.small
57
+ thead
58
+ tr
59
+ th Field
60
+ th Required
61
+ th Meaning
62
+ tbody
63
+ tr
64
+ td: code coordinatorUrl
65
+ td Yes
66
+ td The Coordinator's #[code publicUrl].
67
+ tr
68
+ td: code type
69
+ td Yes
70
+ td #[code hw] (physical DUT) or #[code sw] (DUT image in Docker).
71
+ tr
72
+ td: code joinKey
73
+ td Yes
74
+ td Must equal the Coordinator's #[code clientJoinKey]. Or set #[code THUB_CLIENT_JOIN_KEY].
75
+ tr
76
+ td: code name
77
+ td No
78
+ td Resource name. Under systemd, the instance name (#[code thub-client@dut1] → #[code dut1]) is used unless an admin renames it on the dashboard.
79
+ tr
80
+ td: code labels
81
+ td No
82
+ td Matched against a job's #[code --board]/#[code --label]. Use #[code board:<name>] for the board.
83
+ tr
84
+ td: code groups
85
+ td No
86
+ td Group ids (from #[a(href="/groups") Groups]) this Client belongs to, for #[code thub run --group].
87
+ tr
88
+ td: code varDir
89
+ td No
90
+ td State directory: tokens, the client id, job workspaces, sockets. Keep the installer's default under systemd.
91
+ tr
92
+ td: code hw-devices.stlinks / uarts / usbs
93
+ td HW
94
+ td Up to 8 of each. An entry is a udev index #[code N] (→ #[code /dev/thub/dut<N>-stlink|uart|usb]), a path, or an object with #[code index]/#[code path], #[code devpath] (USB port), #[code serial] (ST-Link) and #[code baudRate] (UART, default 115200).
95
+ tr
96
+ td: code heartbeatIntervalSec / longPollWaitSec
97
+ td No
98
+ td 10 s / 30 s by default.
99
+ tr
100
+ td: code clientId
101
+ td No
102
+ td An explicit identity UUID (or #[code THUB_CLIENT_ID]). Normally generated once and kept in #[code .client-id].
103
+ p.small.
104
+ SW Clients have no settings of their own: they run whatever DUT image a job brings (#[code --docker-image]).
105
+ Admins can also edit an HW Client's devices from its resource card (#[strong ST-Link | UART | DUT USB]
106
+ tabs). #[strong Connected USB devices → Import to config] fills them from the host's #[code lsusb].
107
+
108
+ h3.h6 Several DUT slots on one machine
109
+ +code('One instance per slot').
110
+ thub-client register --name dut1 --type hw # creates ~/.config/thub/dut1.json, starts thub-client@dut1
111
+ nano ~/.config/thub/dut1.json # set that slot's hw-devices
112
+ sudo systemctl restart thub-client@dut1
113
+ thub-client deregister --name dut1 # when the slot goes away
114
+
115
+ h3.h6 Controlling a Client
116
+ +code('As the Client\'s user (not with sudo)').
117
+ thub-client status
118
+ thub-client lock --reason "debugging I2C" # BUSY (local): nothing gets scheduled here
119
+ thub-client unlock
120
+ thub-client restart
121
+ thub-client --config ~/.config/thub/dut1.json status # a specific instance
122
+ thub-client udev --print # the udev rules this config generates
@@ -0,0 +1,134 @@
1
+ +section('coordinator', 'Coordinator setup', 'hdd-network')
2
+ p.
3
+ One Coordinator serves the whole network. It's a single Node.js process with SQLite, which is
4
+ enough for tens of Clients and hundreds of jobs a day. Run it on a small Linux VM (Ubuntu recommended)
5
+ with Node.js 20+ (24 LTS recommended), reachable over HTTPS by the Agents and Clients.
6
+
7
+ h3.h6 1. Install as a systemd service
8
+ +code('Coordinator host').
9
+ sudo apt install -y nodejs npm
10
+ sudo npm i -g @andrian.yablonskyy/thub-coordinator
11
+ p.small.
12
+ #[code sudo npm i -g] does the whole setup for the user who ran #[code sudo]. It creates
13
+ #[code ~/.config/thub/coordinator.json] (with a random #[code sessionSecret]) and
14
+ #[code ~/var/lib/thub] (database, artifacts, avatars). It installs, enables and starts
15
+ #[code thub-coordinator.service], plus the root helper behind the navbar's #[strong Update app] button.
16
+
17
+ h3.h6 2. Configure
18
+ p.small.
19
+ Edit #[code ~/.config/thub/coordinator.json], then #[code sudo systemctl restart thub-coordinator].
20
+ Logs: #[code journalctl -u thub-coordinator -f].
21
+ +code('~/.config/thub/coordinator.json').
22
+ {
23
+ "listen": "127.0.0.1:8080",
24
+ "publicUrl": "#{coordinatorUrl}",
25
+ "trustProxy": "loopback",
26
+ "dataDir": "/home/thub/var/lib/thub",
27
+ "sessionSecret": "<long random string>",
28
+ "clientJoinKey": "<output of: thub-admin join-key generate>",
29
+ "heartbeat": { "intervalSec": 10, "missedLimit": 3, "sweepIntervalSec": 5 },
30
+ "scheduler": { "assignAckTimeoutSec": 15, "requeueOnLost": true, "maxQueuedPerAgent": 20, "tickIntervalSec": 10 },
31
+ "jobs": { "defaultTimeoutSec": 1800, "maxTimeoutSec": 14400 },
32
+ "retention": { "logRetentionDays": 14, "artifactRetentionDays": 30, "jobRetention": "forever" },
33
+ "artifacts": { "maxUploadMb": 512, "linkTtlHours": 168 },
34
+ "updates": { "checkIntervalMin": 15, "registry": null }
35
+ }
36
+ .table-responsive
37
+ table.table.table-sm.small
38
+ thead
39
+ tr
40
+ th Field
41
+ th Meaning
42
+ tbody
43
+ tr
44
+ td: code listen
45
+ td Address the Node.js process binds to. Keep it on #[code 127.0.0.1] behind a reverse proxy.
46
+ tr
47
+ td: code publicUrl
48
+ td The URL Agents, Clients and job links use, e.g. #[code https://thub.example.com].
49
+ tr
50
+ td: code trustProxy
51
+ td Which proxies' #[code X-Forwarded-For] to trust for a Client's external address: #[code loopback] (default), a CIDR such as #[code "10.0.0.0/8"], a hop count, or #[code false].
52
+ tr
53
+ td: code sessionSecret
54
+ td Signs dashboard sessions and artifact download links. Keep it secret and never use the #[code change-me-…] placeholder.
55
+ tr
56
+ td: code clientJoinKey
57
+ td The shared secret Clients self-register with. Unset or #[code null] turns registration off (#[code 503]).
58
+ tr
59
+ td: code heartbeat.*
60
+ td Clients heartbeat every #[code intervalSec]. After #[code missedLimit] misses a resource goes #[code OUT_OF_SERVICE] and its job becomes #[code LOST].
61
+ tr
62
+ td: code scheduler.*
63
+ td #[code assignAckTimeoutSec]: how long an assignment waits for the Client to accept. #[code requeueOnLost]: retry a #[code LOST] job once. #[code maxQueuedPerAgent]: queue limit per agent.
64
+ tr
65
+ td: code jobs.*
66
+ td Default and maximum job timeout, in seconds.
67
+ tr
68
+ td: code retention.*
69
+ td #[code jobRetention]: #[code 1w], #[code 2w], #[code 1m], #[code 3m], #[code 6m] or #[code forever]. Older finished jobs, their logs and artifacts are deleted hourly.
70
+ tr
71
+ td: code updates.*
72
+ td How often to check npm for new versions (#[code 0] turns the periodic check off), and an optional private registry.
73
+ p.small.
74
+ Resolution order: #[code THUB_COORDINATOR_CONFIG] (a path) → #[code ~/.config/thub/coordinator.json] → the bundled
75
+ default. These environment variables override the file: #[code THUB_LISTEN], #[code THUB_PUBLIC_URL],
76
+ #[code THUB_DATA_DIR], #[code THUB_SESSION_SECRET] and #[code THUB_CLIENT_JOIN_KEY].
77
+
78
+ h3.h6 3. HTTPS reverse proxy
79
+ p.small.
80
+ Expose only port 443. Log streams use Server-Sent Events, so the proxy must not buffer responses.
81
+ .row.g-3
82
+ .col-md-6
83
+ +code('Caddyfile').
84
+ thub.example.com {
85
+ reverse_proxy 127.0.0.1:8080 {
86
+ flush_interval -1
87
+ }
88
+ }
89
+ .col-md-6
90
+ +code('nginx').
91
+ location / {
92
+ proxy_pass http://127.0.0.1:8080;
93
+ proxy_set_header Host $host;
94
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
95
+ proxy_set_header X-Forwarded-Proto $scheme;
96
+ proxy_http_version 1.1;
97
+ proxy_buffering off; # SSE log streams
98
+ proxy_read_timeout 1h;
99
+ client_max_body_size 512m; # artifacts.maxUploadMb
100
+ }
101
+
102
+ h3.h6 4. Users, agents and the join key
103
+ p.small.
104
+ #[code thub-admin] works directly on the database. Run it on the Coordinator host as the service user.
105
+ +code('Coordinator host').
106
+ thub-admin create-admin alice 's3cret' --role admin # or --role viewer (read-only dashboard)
107
+ thub-admin join-key generate # → clientJoinKey here, joinKey on every Client
108
+ thub-admin agent add ci-firmware --kind ci # CI token: job ids A-00001…
109
+ thub-admin agent add alice-laptop --kind cli # developer token: job ids M-00001…
110
+ thub-admin group add ci-nightly --comment "nightly pool" # → a group id for thub run --group
111
+ thub-admin resource maintenance <resourceId> --on
112
+ thub-admin jobs reset --yes # cancel every active job
113
+ thub-admin jobs clean --yes --before "01/09/2026 00:00:00"
114
+ +note('warning').
115
+ Forgot the admin password? Start the Coordinator once with #[code THUB_BOOTSTRAP_ADMIN_PASSWORD=…]
116
+ (and optionally #[code THUB_BOOTSTRAP_ADMIN_USER], default #[code admin]). The password is reset on
117
+ #[em every] start while the variable is set, so unset it once you're signed in again.
118
+
119
+ h3.h6 Running from a checkout / DEV mode
120
+ +code('Repository checkout').
121
+ git clone --recurse-submodules git@github.com:andrianyablonskyy/thub.git && cd thub && npm install
122
+ THUB_BOOTSTRAP_ADMIN_PASSWORD=admin ./bin/coordinator
123
+ # Dashboard without hardware: virtual agent, two virtual Clients and demo jobs
124
+ DEV_MODE=1 THUB_BOOTSTRAP_ADMIN_PASSWORD=admin npm run coordinator
125
+ p.small.text-body-secondary.
126
+ In DEV mode the agent token is #[code agt_dev-virtual-agent-token], which is public. Never set
127
+ #[code DEV_MODE] on a real Coordinator.
128
+
129
+ h3.h6 Updates
130
+ p.small.mb-0.
131
+ The Coordinator checks npm for new Coordinator, Agent and Client versions. Admins update the Coordinator
132
+ with #[strong Update app] in the navbar, and Agents and Clients from the #[a(href="/admin/agents") Agents]
133
+ and #[a(href="/resources") Resources] pages. Clients install an update only between jobs.
134
+ Manual alternatives: #[code thub-admin self-update], #[code thub self-update] and #[code thub-client self-update].
@@ -0,0 +1,124 @@
1
+ +section('docker', 'Docker registry integration', 'box-seam')
2
+ p.
3
+ A job can use Docker in two different ways. Keep them apart, because they log in and run differently:
4
+ .row.g-3.mb-3
5
+ .col-md-6
6
+ .card.h-100
7
+ .card-body.small
8
+ h3.h6 #[code --docker-image]: the DUT
9
+ p.mb-0.
10
+ SW jobs only. The Client pulls the image and runs it as the job's #[strong DUT container]
11
+ (an emulator the tests talk to), sandboxed, next to your command. The pull uses the
12
+ #[strong Client host's] Docker login, made once by the service user.
13
+ .col-md-6
14
+ .card.h-100
15
+ .card-body.small
16
+ h3.h6 #[code docker …] inside #[code --command]
17
+ p.mb-0.
18
+ Any job type, on a host with Docker. Your command logs in, pulls and runs containers
19
+ itself, e.g. to run the tests #[em inside] an image. Credentials come from the job
20
+ (#[code --env]) and are scoped to the job.
21
+
22
+ h3.h6 Log in to a registry on the Client host (for --docker-image)
23
+ p.small.
24
+ #[code --docker-image] is pulled with #[code docker pull] as the Client's service user, from the registry the reference names
25
+ (Docker Hub for a short name such as #[code python:3.14]). Log in once per host, as that user:
26
+ +code('Client host').
27
+ # the user that runs thub-client (the one who ran sudo npm i -g), e.g. thub:
28
+ sudo -u thub docker login registry.lab.local:5000
29
+ sudo -u thub docker login ghcr.io -u <github-user> # password: a PAT with read:packages
30
+ sudo -u thub docker pull registry.lab.local:5000/dut-emulator:2026.08 # check it works
31
+ p.small.
32
+ A plain-HTTP registry must be listed in the Docker daemon's #[code insecure-registries]:
33
+ +code('/etc/docker/daemon.json').
34
+ { "insecure-registries": ["registry.lab.local:5000"] }
35
+ p.small.
36
+ Then run #[code sudo systemctl restart docker]. An image that can't be pulled ends the job as #[code ERROR]
37
+ during #[em prepare], naming the registry and Docker's reason. An image already on the host isn't pulled again.
38
+
39
+ h3.h6 How the Client starts the DUT container
40
+ p.small.
41
+ You don't pass #[code docker run] parameters for the DUT. The Client always starts it like this (image default command,
42
+ sandboxed), and removes it and its network when the job ends:
43
+ +code('What the Client runs (equivalent)').
44
+ docker pull registry.lab.local:5000/dut-emulator:2026.08
45
+ docker network create --driver bridge thub-job-<jobId>
46
+ docker run -d --name thub-<jobId> --network thub-job-<jobId> \
47
+ --memory 2g --cpus 2 --read-only \
48
+ -v <workDir>/<jobId>/downloads:/downloads:ro \
49
+ -p 127.0.0.1::5555 registry.lab.local:5000/dut-emulator:2026.08
50
+ ul.small
51
+ li The image must #[strong start the DUT by itself] (its #[code CMD]/#[code ENTRYPOINT]) and should listen on port #[strong 5555].
52
+ li The job's #[code --download-file] files are mounted read-only at #[code /downloads] (e.g. the firmware to emulate).
53
+ li The command reaches it at #[code $THUB_DUT_HOST] (#[code 127.0.0.1:<port>]) and by name as #[code $THUB_DUT_CONTAINER].
54
+ li Its output is shipped as the #[code emulator] log stream. Its root filesystem is read-only: don't install or clone into it.
55
+ li #[code thub run --dry-run] prints the exact docker commands a job would run, without running anything.
56
+
57
+ h3.h6 Execute commands in the DUT container
58
+ +code('SW job: talk to the DUT').
59
+ thub run --type sw \
60
+ --docker-image registry.lab.local:5000/dut-emulator:2026.08 \
61
+ --download-file https://artifactory.example.com/fw-local/app/1.4.0-42/app.elf \
62
+ --command 'docker exec "$THUB_DUT_CONTAINER" /opt/emu/status &&
63
+ docker logs "$THUB_DUT_CONTAINER" | tail -n 20 &&
64
+ ./ci/test.sh --dut "$THUB_DUT_HOST"' \
65
+ --wait
66
+
67
+ h3.h6 Log in and pull inside a job (credentials scoped to the job)
68
+ p.small.
69
+ Pass the credentials as #[code --env] (masked everywhere, dropped when the job ends). Set #[code DOCKER_CONFIG] to the
70
+ job's work directory so the login is deleted with the workspace instead of staying in the service user's
71
+ #[code ~/.docker] for every later job:
72
+ +code('Any job type').
73
+ export DOCKER_PASSWORD=… # from your CI secret store, never on the command line
74
+ thub run --type hw \
75
+ --env DOCKER_REGISTRY=registry.lab.local:5000,DOCKER_USER=ci --env DOCKER_PASSWORD \
76
+ --command 'export DOCKER_CONFIG="$THUB_WORK_DIR/.docker" &&
77
+ echo "$DOCKER_PASSWORD" | docker login "$DOCKER_REGISTRY" -u "$DOCKER_USER" --password-stdin &&
78
+ docker pull "$DOCKER_REGISTRY/team/test-runner:1.4"' \
79
+ --wait
80
+
81
+ h3.h6 Run the tests inside a container (docker run parameters)
82
+ p.small.
83
+ The command always starts on the Client host, in #[code $THUB_WORK_DIR] (the #[code --git-repo] checkout). Mount it,
84
+ hand over the variables the tests need with #[code -e NAME], and use #[code --rm]:
85
+ +code('Tests in an image').
86
+ thub run --type sw \
87
+ --env DOCKER_REGISTRY=registry.lab.local:5000,DOCKER_USER=ci --env DOCKER_PASSWORD \
88
+ --env TARGET=staging \
89
+ --git-repo git@github.com:yourorg/web-ui-tests.git main \
90
+ --command 'export DOCKER_CONFIG="$THUB_WORK_DIR/.docker" &&
91
+ echo "$DOCKER_PASSWORD" | docker login "$DOCKER_REGISTRY" -u "$DOCKER_USER" --password-stdin &&
92
+ docker run --rm \
93
+ -v "$THUB_WORK_DIR:/work" -w /work \
94
+ -e TARGET -e THUB_SUITE -e THUB_JOB_ID \
95
+ --network host \
96
+ "$DOCKER_REGISTRY/python:3.14" ./run-tests.sh --junit results/junit.xml' \
97
+ --wait
98
+ table.table.table-sm.small
99
+ thead
100
+ tr
101
+ th docker run option
102
+ th Why
103
+ tbody
104
+ tr
105
+ td: code --rm
106
+ td Removes the container when the tests end.
107
+ tr
108
+ td: code -v "$THUB_WORK_DIR:/work" -w /work
109
+ td The checkout and downloads, and the #[code results/] and #[code artifacts/] folders, which the Client uploads.
110
+ tr
111
+ td: code -e NAME
112
+ td Passes a job variable (#[code --env], #[code THUB_*], #[code JOB_*]) into the container.
113
+ tr
114
+ td: code --network host
115
+ td Lets the container reach the DUT at #[code $THUB_DUT_HOST] (#[code 127.0.0.1:<port>]).
116
+ tr
117
+ td: code --device /dev/thub/dut1-uart
118
+ td HW: gives the container the DUT's UART (#[code "$THUB_DUT_UART"]). Use #[code --privileged] only if you must.
119
+ tr
120
+ td: code --user "$(id -u):$(id -g)"
121
+ td Files written to #[code /work] stay owned by the Client user, so the workspace can be cleaned up.
122
+ +note('info').
123
+ Docker access is effectively root access on that host. The Client host must have Docker and the service user must be in the
124
+ #[code docker] group. The resource card's #[strong Capabilities] show whether a Client can use Docker, and why not.
@@ -0,0 +1,127 @@
1
+ +section('env', 'Environment variables on the Client', 'braces')
2
+ p.
3
+ #[code --command] runs on the Client with the service's own environment (#[code PATH], #[code HOME], …) plus three sets of
4
+ variables. Every value is a string. A variable whose option wasn't given is #[strong unset], so use
5
+ #[code ${VAR:-default}] in shell. A repeatable option gives one combined variable plus #[code NAME_1], #[code NAME_2], …
6
+ (1-based, in the order given).
7
+
8
+ h3.h6 Set by the Client: #[code THUB_*]
9
+ -
10
+ const thubVars = [
11
+ ['THUB_JOB_ID', 'The job id, e.g. M-00125'],
12
+ ['THUB_WORK_DIR', 'The directory the command runs in: the --git-repo checkout, else empty. results/ and artifacts/ in it are uploaded'],
13
+ ['THUB_SUITE', '--suite (default: default)'],
14
+ ['THUB_GIT_COMMIT', 'The exact commit checked out for --git-repo'],
15
+ ['THUB_DOWNLOADS_DIR', 'Where the --download-file files are'],
16
+ ['THUB_DOWNLOADS / THUB_DOWNLOAD_<n>', 'Local paths of the downloads: all, one per line / each one'],
17
+ ['THUB_META_<KEY>', '--meta values; camelCase keys become SNAKE_CASE (ciJobId → THUB_META_CI_JOB_ID)'],
18
+ ['THUB_DUT_STLINK / _<n>', 'HW: ST-Link serials (device path if the serial couldn\'t be read); unsuffixed = the first'],
19
+ ['THUB_DUT_UART / _<n>', 'HW: UART device paths (/dev/thub/dut<N>-uart); unsuffixed = the first'],
20
+ ['THUB_DUT_USB / _<n>', 'HW: DUT USB device paths'],
21
+ ['THUB_DUT_HOST', 'SW with --docker-image: 127.0.0.1:<port> of the DUT container\'s port 5555'],
22
+ ['THUB_DUT_CONTAINER', 'SW with --docker-image: the DUT container\'s name (docker exec / docker logs)']
23
+ ]
24
+ .table-responsive
25
+ table.table.table-sm.small
26
+ tbody
27
+ each v in thubVars
28
+ tr
29
+ td(style="width: 34%"): code= v[0]
30
+ td= v[1]
31
+
32
+ h3.h6 Job parameters: #[code JOB_*]
33
+ p.small Each #[code thub run] option that reaches the Client, as given:
34
+ -
35
+ const jobVars = [
36
+ ['JOB_TYPE', '--type', 'hw or sw'],
37
+ ['JOB_BOARD', '--board', 'the board name'],
38
+ ['JOB_LABEL / JOB_LABEL_<n>', '--board, --label', 'all labels, comma-separated / each'],
39
+ ['JOB_GROUP', '--group', 'the group id'],
40
+ ['JOB_CLIENT', '--client', 'this Client\'s name'],
41
+ ['JOB_USER', '--user', 'as given'],
42
+ ['JOB_COMMAND', '--command', 'the command itself'],
43
+ ['JOB_DOWNLOAD_FILE / _<n>', '--download-file', 'the URLs (the local files are THUB_DOWNLOAD_<n>)'],
44
+ ['JOB_DOCKER_IMAGE', '--docker-image', 'the image reference'],
45
+ ['JOB_GIT_REPO_URL', '--git-repo <url>', 'the repository URL'],
46
+ ['JOB_GIT_BRANCH', '--git-repo <url> <ref>', 'branch, tag or commit as given (unset = default branch)'],
47
+ ['JOB_GIT_DEPTH', '--depth', 'commits fetched (1 by default, 0 = full)'],
48
+ ['JOB_GIT_OPTIONS', '--git-options', 'as given'],
49
+ ['JOB_SUITE', '--suite', 'as given, default when not'],
50
+ ['JOB_ARG / JOB_ARG_<n>', '--arg', 'all, space-separated / each (also "$@")'],
51
+ ['JOB_TIMEOUT', '--timeout', 'seconds'],
52
+ ['JOB_PRIORITY', '--priority', '0–100'],
53
+ ['JOB_META_<KEY>', '--meta key=value', 'the value'],
54
+ ['<NAME>', '--env NAME=value', 'your own variables, under the names you chose']
55
+ ]
56
+ .table-responsive
57
+ table.table.table-sm.small
58
+ thead
59
+ tr
60
+ th Variable
61
+ th From
62
+ th Value
63
+ tbody
64
+ each v in jobVars
65
+ tr
66
+ td: code= v[0]
67
+ td: code.text-nowrap= v[1]
68
+ td= v[2]
69
+ p.small.
70
+ #[code --url], #[code --token], #[code --wait], #[code --detach], #[code --json] and #[code --dry-run] only affect the Agent and
71
+ have no variable. The Client's own git commands also get #[code GIT_TERMINAL_PROMPT=0] and #[code GIT_ALLOW_PROTOCOL=http:https:ssh:git].
72
+
73
+ h3.h6 Your own variables: #[code --env]
74
+ ul.small
75
+ li #[code --env NAME=value[,NAME=value]], repeatable. A comma starts a new variable only when #[code NAME=] follows it, so values may contain commas.
76
+ li #[code --env NAME] alone takes the value from the Agent's environment. That keeps secrets off the command line, out of shell history and out of CI logs.
77
+ li Any names except #[code THUB_*], #[code JOB_*], #[code GIT_TERMINAL_PROMPT] and #[code GIT_ALLOW_PROTOCOL].
78
+ li They apply to every command the Client runs for the job: the #[code --git-repo] git commands and #[code --command].
79
+ li
80
+ | Every value is treated as a secret. It's sent only to the Client running the job, shown as #[code ***] by the API,
81
+ | #[code thub status --json] and #[code --dry-run], and wiped from the Coordinator's database when the job ends.
82
+ | The dashboard lists only the names.
83
+ +note('warning').
84
+ The command's own output #[strong is] the job log, which is stored and visible on the dashboard. Never #[code echo] a
85
+ secret, and avoid #[code set -x] in scripts that use one.
86
+
87
+ h3.h6 Using them in your scripts
88
+ +code('ci/test.sh (shell)').
89
+ #!/bin/sh
90
+ set -eu
91
+ echo "Job $THUB_JOB_ID on $(hostname), suite ${THUB_SUITE}, commit ${THUB_GIT_COMMIT:-n/a}"
92
+
93
+ # optional parameters with defaults
94
+ TARGET="${TARGET:-staging}"
95
+ BRANCH="${JOB_GIT_BRANCH:-main}"
96
+
97
+ # every downloaded file
98
+ printf '%s\n' "$THUB_DOWNLOADS" | while read -r f; do echo "downloaded: $f"; done
99
+
100
+ # HW: flash through the first ST-Link, talk to the second UART
101
+ st-flash --serial "$THUB_DUT_STLINK" --reset write "$THUB_DOWNLOAD_1" 0x08000000
102
+ python3 -m pytest tests/ --uart "${THUB_DUT_UART_2:-$THUB_DUT_UART}" --junitxml=results/junit.xml
103
+
104
+ # CI metadata passed with --meta ciJobId=… / --meta sha=…
105
+ echo "CI run ${THUB_META_CI_JOB_ID:-local}, sha ${THUB_META_SHA:-unknown}"
106
+ +code('conftest.py (Python)').
107
+ import os
108
+
109
+ DUT_HOST = os.environ.get("THUB_DUT_HOST", "127.0.0.1:5555") # SW DUT container
110
+ UARTS = [v for k, v in sorted(os.environ.items()) if k.startswith("THUB_DUT_UART_")]
111
+ FIRMWARE = os.environ.get("THUB_DOWNLOAD_1")
112
+ RESULTS = os.path.join(os.environ.get("THUB_WORK_DIR", "."), "results")
113
+ API_TOKEN = os.environ["API_TOKEN"] # from --env API_TOKEN
114
+ +code('Into a container: -e NAME copies a variable from the job').
115
+ docker run --rm -v "$THUB_WORK_DIR:/work" -w /work \
116
+ -e THUB_JOB_ID -e THUB_SUITE -e TARGET -e API_TOKEN \
117
+ python:3.14 ./run-tests.sh
118
+ +code('Submitting the job that feeds these').
119
+ export API_TOKEN=…
120
+ thub run --type hw --board nucleo-f401re \
121
+ --download-file https://artifactory.example.com/fw-local/app/1.4.0-42/app.bin \
122
+ --git-repo git@github.com:yourorg/firmware-tests.git main \
123
+ --env TARGET=production --env API_TOKEN \
124
+ --meta ciJobId="$GITHUB_RUN_ID" --meta sha="$GITHUB_SHA" \
125
+ --suite smoke --command ./ci/test.sh --wait
126
+ p.small.text-body-secondary.mb-0.
127
+ Tip: #[code thub run … --dry-run] lists every variable the command would get on the chosen Client, with #[code --env] values as #[code ***].
@@ -0,0 +1,129 @@
1
+ +section('git', 'Git repository integration', 'git')
2
+ p.
3
+ #[code --git-repo] makes the Client clone a repository before the job runs. #[code --command] then runs inside the checkout
4
+ (#[code $THUB_WORK_DIR]). Typically the repository holds the test scripts. The firmware comes as a #[code --download-file]
5
+ or is built by the command.
6
+
7
+ h3.h6 1. Create a test repository
8
+ +code('Your machine').
9
+ mkdir firmware-tests && cd firmware-tests && git init -b main
10
+ mkdir -p ci
11
+ cat > ci/test.sh &lt;&lt;'EOF'
12
+ #!/bin/sh
13
+ set -eu
14
+ echo "job $THUB_JOB_ID, suite $THUB_SUITE, commit $THUB_GIT_COMMIT"
15
+ mkdir -p results
16
+ pytest --junitxml=results/junit.xml tests/ # results/*.xml is uploaded and summarized
17
+ EOF
18
+ chmod +x ci/test.sh
19
+ git add . && git commit -m "TestHub entry point"
20
+ git remote add origin git@github.com:yourorg/firmware-tests.git
21
+ git push -u origin main
22
+ p.small.
23
+ The exit code of #[code --command] is the verdict. JUnit files under #[code results/] or #[code artifacts/] are uploaded
24
+ as artifacts and summed into the job's test counts. Any other file there is uploaded too.
25
+
26
+ h3.h6 2. Give Clients read access: SSH deploy key (recommended)
27
+ p.small.
28
+ git never prompts on a Client (#[code GIT_TERMINAL_PROMPT=0]), so credentials must already be on the Client host,
29
+ owned by the #[strong Client's service user]. Create a key on each Client host (or one shared key per lab):
30
+ +code('Client host, as the Client\'s user').
31
+ ssh-keygen -t ed25519 -N "" -C "thub-client@$(hostname)" -f ~/.ssh/thub_deploy
32
+ cat ~/.ssh/thub_deploy.pub # → add as a read-only deploy key (below)
33
+ # trust the git server's host key now: the service can't answer prompts
34
+ # (and can't write ~/.ssh under systemd)
35
+ ssh-keyscan github.com bitbucket.org gitlab.com >> ~/.ssh/known_hosts
36
+ # use the key for that server
37
+ cat >> ~/.ssh/config &lt;&lt;'EOF'
38
+ Host github.com
39
+ IdentityFile ~/.ssh/thub_deploy
40
+ IdentitiesOnly yes
41
+ EOF
42
+ chmod 600 ~/.ssh/config
43
+ ssh -T git@github.com # "…successfully authenticated…"
44
+ table.table.table-sm.small
45
+ thead
46
+ tr
47
+ th Git server
48
+ th Where to add the public key
49
+ tbody
50
+ tr
51
+ td GitHub
52
+ td Repository → Settings → Deploy keys → Add deploy key (leave “Allow write access” off). For many repositories, use a machine user's SSH key instead.
53
+ tr
54
+ td GitLab
55
+ td Project → Settings → Repository → Deploy keys.
56
+ tr
57
+ td Bitbucket
58
+ td Repository settings → Security → Access keys.
59
+ tr
60
+ td Gitea / self-hosted
61
+ td Repository → Settings → Deploy Keys, or the machine user's SSH keys.
62
+ p.small.
63
+ Without #[code ~/.ssh/config], pick the key per job with #[code --git-options] (stored with the job, so reference a key
64
+ #[em file], never inline a secret):
65
+ +code('Per-job SSH key / port').
66
+ thub run --type sw \
67
+ --git-repo ssh://git@git.lab.local:2222/qa/tests.git main \
68
+ --git-options '-c core.sshCommand="ssh -i ~/.ssh/thub_deploy -o IdentitiesOnly=yes"' \
69
+ --command ./ci/test.sh --wait
70
+
71
+ h3.h6 HTTPS with a token
72
+ p.small.
73
+ For #[code https://] repositories, either store a credential helper for the Client's user on the host
74
+ (#[code git config --global credential.helper store]), or pass the token #[strong per job] as a secret. #[code --env]
75
+ variables reach the Client's own git commands, and git reads config from #[code GIT_CONFIG_COUNT]/#[code _KEY_n]/#[code _VALUE_n]:
76
+ +code('Token per job, never stored').
77
+ # GitHub: user x-access-token; GitLab: oauth2; Bitbucket: x-token-auth
78
+ export GIT_CONFIG_VALUE_0="Authorization: Basic $(printf 'x-access-token:%s' "$GH_TOKEN" | base64 | tr -d '\n')"
79
+ thub run --type sw \
80
+ --git-repo https://github.com/yourorg/private-tests.git main \
81
+ --env GIT_CONFIG_COUNT=1,GIT_CONFIG_KEY_0=http.extraHeader --env GIT_CONFIG_VALUE_0 \
82
+ --command ./ci/test.sh --wait
83
+ p.small.
84
+ Don't put a token in the URL, #[code --git-options], #[code --command] or #[code --meta]: those are stored with the job
85
+ and visible to anyone who can read it. Only #[code --env] values are masked.
86
+
87
+ h3.h6 3. Clone options: ref, branch, tag, commit, depth
88
+ +code('--git-repo <url> [<branch>|<tag>|<commit>] [--depth <n>]').
89
+ --git-repo git@github.com:yourorg/tests.git # default branch, depth 1
90
+ --git-repo git@github.com:yourorg/tests.git develop # a branch
91
+ --git-repo git@github.com:yourorg/tests.git v1.4.0 # a tag
92
+ --git-repo git@github.com:yourorg/tests.git a1b2c3d # a commit (may be abbreviated)
93
+ --git-repo git@github.com:yourorg/tests.git main --depth 50 # last 50 commits
94
+ --git-repo git@github.com:yourorg/tests.git main --depth 0 # full history (git describe, changelogs)
95
+ table.table.table-sm.small
96
+ tbody
97
+ tr
98
+ th Transports
99
+ td #[code https://], #[code http://], #[code ssh://], #[code git://], #[code user@host:path]. #[code file://] and #[code ext::] are refused.
100
+ tr
101
+ th Default ref
102
+ td The repository's default branch.
103
+ tr
104
+ th Depth
105
+ td Default #[code 1]. #[code 0] = full history. If the server can't do shallow fetches (or the commit is abbreviated), the Client falls back to a full fetch.
106
+ tr
107
+ th Exact commit
108
+ td Logged, and passed to the command as #[code $THUB_GIT_COMMIT]. The requested ref is #[code $JOB_GIT_BRANCH].
109
+ tr
110
+ th Extra git options
111
+ td #[code --git-options] is inserted between #[code git] and its subcommand on every git call, e.g. #[code '-c http.sslVerify=false'] or #[code '-c core.sshCommand="…"'].
112
+ +code('What the Client runs (see it with --dry-run)').
113
+ git -C &lt;work> init -q
114
+ git -C &lt;work> remote add origin &lt;url>
115
+ git -C &lt;work> fetch -q --depth 1 origin &lt;ref>
116
+ git -C &lt;work> checkout -q --detach FETCH_HEAD
117
+ git -C &lt;work> rev-parse HEAD
118
+
119
+ h3.h6 Submodules, LFS and cloning inside a container
120
+ p.small.
121
+ The Client checks out only the repository itself. Do anything more in the command, which runs in the checkout with the same #[code --env] variables:
122
+ +code('Submodules / LFS').
123
+ thub run --type sw --git-repo git@github.com:yourorg/tests.git main \
124
+ --command 'git submodule update --init --recursive --depth 1 && git lfs pull && ./ci/test.sh' --wait
125
+ +code('Clone yourself, e.g. inside a container (JOB_GIT_* carry the --git-repo values)').
126
+ thub run --type sw --git-repo git@github.com:yourorg/tests.git main --depth 1 \
127
+ --command 'docker run --rm -v "$HOME/.ssh:/root/.ssh:ro" -e JOB_GIT_REPO_URL -e JOB_GIT_BRANCH -e JOB_GIT_DEPTH \
128
+ --entrypoint sh alpine/git -c "git clone --depth \$JOB_GIT_DEPTH --branch \$JOB_GIT_BRANCH \$JOB_GIT_REPO_URL /src && ls /src"' \
129
+ --wait
@@ -0,0 +1,24 @@
1
+ //- Help page building blocks. Code blocks are plain text: escape `<` as
2
+ //- `&lt;` inside them (`>` and `&&` are fine). public/js/help.js adds a copy
3
+ //- button to each, which copies the decoded text.
4
+ mixin code(caption)
5
+ figure.thub-code.mb-3
6
+ if caption
7
+ figcaption.thub-code-caption= caption
8
+ pre
9
+ code
10
+ block
11
+
12
+ //- One section of the help page: an anchor for the table of contents and a
13
+ //- heading. `data-help-section` is what the filter box searches.
14
+ mixin section(id, heading, icon)
15
+ section.thub-help-section.mb-5(id=id data-help-section)
16
+ h2.h4.border-bottom.pb-2.mb-3
17
+ if icon
18
+ i.bi.me-2.text-body-secondary(class=`bi-${icon}`)
19
+ = heading
20
+ block
21
+
22
+ mixin note(tone)
23
+ .alert.small.py-2(class=`alert-${tone || 'info'}`)
24
+ block