@andrian.yablonskyy/thub-coordinator 1.1.12 → 1.1.14

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.
@@ -23,20 +23,15 @@
23
23
  ['--client <name|id>', 'Only this Client; waits for it even if others are idle.', '--client lab-hw-01']
24
24
  ] },
25
25
  { title: 'What to run', rows: [
26
- ['--command <string>', 'Required. Run with sh -c in the work directory. Exit code 0 = PASSED.', "--command './ci/test.sh'"],
26
+ ['--command <string>', 'Required. Run with sh -c in the work directory. Exit code 0 = PASSED. It clones repositories and runs containers itself (see Using git / Using Docker).', "--command './ci/test.sh'"],
27
27
  ['--arg <value>', 'Argument for the command, as "$@" (repeatable).', '--arg --junit --arg -v'],
28
- ['--suite <name>', 'Passed as THUB_SUITE (default: default).', '--suite smoke'],
28
+ ['--suite <name>', 'Passed as JOB_SUITE (default: default).', '--suite smoke'],
29
29
  ['--download-file <url>', 'File downloaded before the command (repeatable, http(s), no credentials). THUB_DOWNLOAD_1…', '--download-file https://…/app.bin'],
30
- ['--git-repo <url> [ref]', 'Repository cloned first; the command runs in it.', '--git-repo git@github.com:org/tests.git v1.4.0'],
31
- ['--depth <n>', 'Commits to fetch with --git-repo (default 1, 0 = full).', '--depth 0'],
32
- ['--git-options <string>', 'Extra git options between git and its subcommand. Stored with the job.', '--git-options \'-c core.sshCommand="ssh -i ~/.ssh/k"\''],
33
- ['--docker-image <ref>', 'SW only: DUT container image, pulled with the Client host’s Docker login.', '--docker-image registry.lab:5000/emu:1'],
34
- ['--env <vars>', 'NAME=value[,NAME=value] for git and the command (repeatable). --env NAME takes the value from your shell. Masked, dropped at job end.', '--env TARGET=staging --env API_TOKEN']
30
+ ['--env <vars>', 'NAME=value[,NAME=value] for the command (repeatable): how data and secrets reach the job (git tokens, registry passwords). --env NAME takes the value from your shell. Masked, dropped at job end.', '--env TARGET=staging --env API_TOKEN']
35
31
  ] },
36
32
  { title: 'Job settings', rows: [
37
33
  ['--timeout <dur>', 'e.g. 30m, 1h (default 30m, capped by the Coordinator).', '--timeout 1h'],
38
34
  ['--priority <n>', '0–100. Defaults: ci 50, cli 60.', '--priority 80'],
39
- ['--user <name>', 'Free-text owner label. Default: THUB_USER / config.', '--user alice'],
40
35
  ['--meta <key=value>', 'Metadata stored on the job (repeatable). The command sees THUB_META_<KEY>.', '--meta ciJobId=$GITHUB_RUN_ID'],
41
36
  ['--dry-run', 'Schedule for real, but only log what the Client would run.', '--dry-run']
42
37
  ] },
@@ -74,12 +69,13 @@
74
69
  +code('Flash and test a board, fail CI on failure').
75
70
  thub run --type hw --label board:nucleo-f401re \
76
71
  --download-file https://artifactory.example.com/fw-local/app/1.4.0-42/app.bin \
77
- --git-repo https://github.com/yourorg/firmware-tests.git v1.4.0 \
78
- --command 'st-flash --serial "$THUB_DUT_STLINK" --reset write "$THUB_DOWNLOAD_1" 0x08000000 && ./ci/test.sh "$@"' \
72
+ --env GH_TOKEN \
73
+ --command 'git clone --depth 1 --branch v1.4.0 "https://x-access-token:$GH_TOKEN@github.com/yourorg/firmware-tests.git" src && cd src &&
74
+ st-flash --reset write "$THUB_DOWNLOAD_1" 0x08000000 && ./ci/test.sh "$@"' \
79
75
  --arg --junit --timeout 45m --wait
80
- +code('Reproduce a failure on the exact bench, labeled with your name').
81
- thub run --type hw --label board:nucleo-f401re --client lab-hw-01 --user "$USER" \
82
- --download-file "$IMAGE_URL" --git-repo "$TESTS_REPO" --command ./ci/test.sh
76
+ +code('Reproduce a failure on the exact bench (the job carries your username)').
77
+ thub run --type hw --label board:nucleo-f401re --client lab-hw-01 \
78
+ --download-file "$IMAGE_URL" --env GH_TOKEN --command 'git clone --depth 1 "https://x-access-token:$GH_TOKEN@$TESTS_REPO" src && cd src && ./ci/test.sh'
83
79
  # Ctrl-C detaches; the job keeps running. Re-attach:
84
80
  thub status M-00126
85
81
  +code('Verify a download before using it').
@@ -87,14 +83,14 @@
87
83
  --command 'echo "8f15bf27… $THUB_DOWNLOAD_1" | sha256sum -c && ./flash.sh "$THUB_DOWNLOAD_1"' --wait
88
84
  +code('Secrets and parameters').
89
85
  export API_TOKEN=…
90
- thub run --type sw --git-repo "$TESTS_REPO" \
86
+ thub run --type sw \
91
87
  --env TARGET=staging,LOG_LEVEL=debug --env API_TOKEN \
92
88
  --command './run-tests.sh --target "$TARGET"' --suite regression --wait
93
89
  +code('Preview exactly what would run (nothing is executed)').
94
- thub run --type sw --docker-image alpine --git-repo https://example.invalid/tests.git main \
90
+ thub run --type sw --download-file https://example.invalid/app.bin \
95
91
  --command ./ci/test.sh --dry-run --wait
96
92
  +code('Scripting: submit, poll, read the result').
97
- JOB=$(thub run --type sw --git-repo "$TESTS_REPO" --command ./ci/test.sh --detach --json | jq -r .jobId)
93
+ JOB=$(thub run --type sw --command ./ci/test.sh --detach --json | jq -r .jobId)
98
94
  while thub status "$JOB" --json > job.json; [ $? -eq 5 ]; do sleep 10; done
99
95
  jq -r '"\(.state) exit=\(.exit_code) tests=\(.summary.total // 0) failed=\(.summary.failed // 0)"' job.json
100
96
  +code('Lists').
@@ -42,7 +42,6 @@
42
42
  +code('Save the settings once').
43
43
  thub config set url #{coordinatorUrl}
44
44
  thub config set key thk_…
45
- thub config set user "Your Name" # optional: job owner label
46
45
  p.small.
47
46
  These are saved in #[code ~/.config/thub/agent.json]. Which runner group your jobs run in isn't an Agent
48
47
  setting: an admin or maintainer sets it for your user on the dashboard (#[code thub whoami] shows it). Order of precedence: flags (#[code --url], #[code --key]) →
@@ -59,9 +58,6 @@
59
58
  tr
60
59
  td: code THUB_KEY
61
60
  td Your access key, or a CI token in a pipeline. Keep it in your CI's secret store. (#[code THUB_TOKEN], #[code --token] and a saved #[code token] still work: the old names.)
62
- tr
63
- td: code THUB_USER
64
- td Default #[code --user]
65
61
  tr
66
62
  td: code THUB_NO_SELF_UPDATE=1
67
63
  td Don't install admin-requested Agent updates on this machine
@@ -33,8 +33,9 @@
33
33
  npx -y @andrian.yablonskyy/thub-agent run \
34
34
  --type hw --label board:nucleo-f401re \
35
35
  --download-file "${{ needs.build.outputs.image_url }}" \
36
- --git-repo "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY.git" "$GITHUB_SHA" \
37
- --command 'st-flash --serial "$THUB_DUT_STLINK" --reset write "$THUB_DOWNLOAD_1" 0x08000000 && ./ci/hw-tests.sh' \
36
+ --env GH_TOKEN="${{ secrets.TESTS_READ_TOKEN }}",SHA="$GITHUB_SHA",REPO="$GITHUB_REPOSITORY" \
37
+ --command 'git init -q src && cd src && git fetch -q --depth 1 "https://x-access-token:$GH_TOKEN@github.com/$REPO.git" "$SHA" && git checkout -q FETCH_HEAD &&
38
+ st-flash --reset write "$THUB_DOWNLOAD_1" 0x08000000 && ./ci/hw-tests.sh' \
38
39
  --suite smoke --timeout 30m --wait \
39
40
  --meta runUrl="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
40
41
  ul.small.mb-0
@@ -26,8 +26,8 @@
26
26
  .card-body.small
27
27
  ul.mb-0
28
28
  li Any Linux host: bare metal, a VM (Proxmox, VMware, Hyper-V, KVM) or a cloud instance.
29
- li Docker Engine (#[code docker.io]), with the Client's service user in the #[code docker] group (the installer arranges this).
30
- li Each job's DUT container is limited to 2 CPUs and 2 GB RAM. Size the VM for one job plus your test tools: 4 vCPU, 8 GB RAM and 40 GB+ disk for images and workspaces is a good start.
29
+ li Whatever your jobs' commands use: git, Docker Engine (#[code docker.io]; install it before the Client so its service user gets the #[code docker] group), an emulator. The Client itself needs none of them.
30
+ li Size the VM for one job plus your test tools: 4 vCPU, 8 GB RAM and 40 GB+ disk for images and workspaces is a good start.
31
31
  li For KVM-accelerated emulators (QEMU) inside a VM, turn on #[strong nested virtualization].
32
32
  li No USB or udev setup.
33
33
 
@@ -52,8 +52,9 @@
52
52
  +code('A typical HW job').
53
53
  thub run --type hw --label board:nucleo-f401re \
54
54
  --download-file https://artifactory.example.com/fw-local/app/1.4.0-42/app.bin \
55
- --git-repo https://github.com/yourorg/firmware-tests.git v1.4.0 \
56
- --command 'st-flash --serial "$THUB_DUT_STLINK" --reset write "$THUB_DOWNLOAD_1" 0x08000000 && ./ci/test.sh' \
55
+ --env GH_TOKEN \
56
+ --command 'git clone --depth 1 --branch v1.4.0 "https://x-access-token:$GH_TOKEN@github.com/yourorg/firmware-tests.git" src && cd src &&
57
+ st-flash --reset write "$THUB_DOWNLOAD_1" 0x08000000 && ./ci/test.sh' \
57
58
  --wait
58
59
 
59
60
  +note('warning').
@@ -62,26 +63,25 @@
62
63
 
63
64
  h3.h6 Preparing an SW machine (real or virtual)
64
65
  +code('SW machine / VM').
65
- sudo apt install -y nodejs npm docker.io
66
+ sudo apt install -y nodejs npm # + git, docker.io, … if your jobs' commands use them
66
67
  sudo npm i -g @andrian.yablonskyy/thub-client
67
68
  nano ~/.config/thub/client.json # "type": "sw", coordinatorUrl, joinKey
68
69
  sudo systemctl enable --now thub-client@client
69
- # a private registry for DUT images? log in once, as the Client's user:
70
- docker login registry.lab.local:5000
71
70
  p.small.
72
71
  To scale out, clone the VM template. In the clone, #[strong before its first start], run
73
72
  #[code rm -f ~/var/lib/thub/client/*/.client-id ~/var/lib/thub/client/*.token] and give it its own #[code name].
74
73
  Otherwise both VMs register as the same runner.
75
74
  +code('A typical SW job').
76
75
  thub run --type sw \
77
- --docker-image registry.lab.local:5000/dut-emulator:2026.08 \
78
- --git-repo git@github.com:yourorg/firmware-tests.git main \
79
- --command 'make test DUT="$THUB_DUT_HOST"' --wait
76
+ --env GH_TOKEN --env DOCKER_PASSWORD \
77
+ --command 'git clone --depth 1 "https://x-access-token:$GH_TOKEN@github.com/yourorg/firmware-tests.git" src && cd src &&
78
+ echo "$DOCKER_PASSWORD" | docker login registry.lab.local:5000 -u ci --password-stdin &&
79
+ docker run --rm --user "$(id -u):$(id -g)" -v "$THUB_WORK_DIR/src:/work" -w /work registry.lab.local:5000/dut-emulator:2026.08 make test' --wait
80
80
 
81
81
  h3.h6 Checklist for any Client machine
82
82
  ul.small.mb-0
83
83
  li Correct time (NTP). Timestamps are anchored to the Coordinator's clock, but TLS needs a sane clock.
84
84
  li Outbound HTTPS to the Coordinator; test it with #[code curl -sI #{coordinatorUrl}/login].
85
- li The Client's service user has SSH keys and git credentials for private repos (#[a(href="#git") Git repositories]) and Docker logins for private registries (#[a(href="#docker") Docker registry]).
85
+ li Whatever tools your jobs' commands use (git, docker, a flasher) are installed. Their credentials come with each job as #[code --env] (#[a(href="#git") Using git], #[a(href="#docker") Using Docker]), not from the host.
86
86
  li Under systemd, the service can only write to its state directory. Jobs write to #[code $THUB_WORK_DIR], not to #[code ~].
87
87
  li Optional: a nightly reboot from the runner card's #[strong Reboot] tab (cron, host-local time). It waits for running jobs.
@@ -8,8 +8,8 @@
8
8
  +code('Lab machine, as the user the Client should run as (via sudo)').
9
9
  # Node.js and HW tools (stlink-tools/openocd only for HW Clients)
10
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.
11
+ # Optional — the Client doesn't need it. Only if your jobs' commands run docker;
12
+ # install it BEFORE thub-client so the service gets the docker group.
13
13
  sudo apt install -y docker.io
14
14
  sudo npm i -g @andrian.yablonskyy/thub-client
15
15
  p.small.
@@ -66,7 +66,7 @@
66
66
  tr
67
67
  td: code type
68
68
  td Yes
69
- td #[code hw] (physical DUT) or #[code sw] (DUT image in Docker).
69
+ td #[code hw] (physical DUT) or #[code sw] (software-only: jobs run just their command).
70
70
  tr
71
71
  td: code joinKey
72
72
  td Yes
@@ -100,14 +100,14 @@
100
100
  td No
101
101
  td An explicit identity UUID (or #[code THUB_CLIENT_ID]). Normally generated once and kept in #[code .client-id].
102
102
  p.small.
103
- SW Clients have no settings of their own: they run whatever DUT image a job brings (#[code --docker-image]).
103
+ SW Clients have no settings of their own: a job's #[code --command] starts whatever it needs (a container, an emulator).
104
104
  Admins can also edit an HW Client's devices from its runner card (#[strong ST-Link | UART | DUT USB]
105
105
  tabs). The tabs show the config the Client runs, as it reported it at its last start, or a saved
106
106
  change it hasn't applied yet. #[strong USB devices → Import to config] fills them from the host's #[code lsusb].
107
107
  The card's #[strong Export] and #[strong Import] icons download a Client's config file (never its
108
108
  #[code joinKey]) and apply one to it. An older HW Client takes only the file's #[code hw-devices].
109
- There's no ST-Link serial field: the job's #[code --command] picks the probe
110
- (#[code $THUB_DUT_STLINK_&lt;n&gt;]), and the Client reads each serial from its udev symlink.
109
+ There's no ST-Link serial field: the job's #[code --command] picks the probe (#[code st-flash] alone uses the
110
+ only one; with several, #[code st-flash --serial] with a serial from #[code st-info --probe]).
111
111
 
112
112
  h3.h6 Several DUT slots on one machine
113
113
  +code('One instance per slot').
@@ -1,99 +1,23 @@
1
- +section('docker', 'Docker registry integration', 'box-seam')
1
+ +section('docker', 'Using Docker in a job', 'box-seam')
2
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.
3
+ The Client doesn't pull images or start containers itself and doesn't need Docker. A job that needs a container runs
4
+ #[code docker] in its #[code --command], on a Client host that has Docker (the runner card's #[strong Capabilities] tab says
5
+ whether it does). Registry credentials come from the job as #[code --env]: masked everywhere, dropped when the job ends.
21
6
 
22
- h3.h6 Log in to a registry on the Client host (for --docker-image)
7
+ h3.h6 Log in, pull and run, credentials scoped to the job
23
8
  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 &lt;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-&lt;jobId>
46
- docker run -d --name thub-&lt;jobId> --network thub-job-&lt;jobId> \
47
- --memory 2g --cpus 2 --read-only \
48
- -v &lt;workDir>/&lt;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:&lt;port&gt;]) 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:
9
+ Point #[code DOCKER_CONFIG] at the job's directory so the login is deleted with the job instead of staying in the
10
+ service user's #[code ~/.docker] for every later job:
72
11
  +code('Any job type').
73
12
  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
13
  thub run --type sw \
87
14
  --env DOCKER_REGISTRY=registry.lab.local:5000,DOCKER_USER=ci --env DOCKER_PASSWORD \
88
15
  --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' \
16
+ --command 'echo "$DOCKER_PASSWORD" | docker login "$DOCKER_REGISTRY" -u "$DOCKER_USER" --password-stdin &&
17
+ docker run --rm --user "$(id -u):$(id -g)" \
18
+ -v "$THUB_WORK_DIR/src:/work" -w /work \
19
+ -e TARGET -e JOB_SUITE -e THUB_JOB_ID \
20
+ "$DOCKER_REGISTRY/team/test-runner:1.4" ./run-tests.sh --junit results/junit.xml' \
97
21
  --wait
98
22
  table.table.table-sm.small
99
23
  thead
@@ -105,20 +29,61 @@
105
29
  td: code --rm
106
30
  td Removes the container when the tests end.
107
31
  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, where the Client reads JUnit XML for the test counts.
32
+ td: code -v "$THUB_WORK_DIR/src:/work" -w /work
33
+ td Only the clone (#[code src/]), not the job directory, which holds the registry login in #[code .docker/]. The Client reads JUnit XML from #[code src/results/] or #[code src/artifacts/] for the test counts.
110
34
  tr
111
35
  td: code -e NAME
112
36
  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:&lt;port&gt;]).
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
37
  tr
120
38
  td: code --user "$(id -u):$(id -g)"
121
39
  td Files written to #[code /work] stay owned by the Client user, so the workspace can be cleaned up.
40
+ tr
41
+ td: code --device /dev/thub/dut1-uart
42
+ td HW: gives the container the DUT's UART (#[code /dev/thub/dut1-uart]). Use #[code --privileged] only if you must.
43
+
44
+ h3.h6 Clone and test inside a container, with a deploy key and a registry login from the job
45
+ p.small.
46
+ Everything comes from the Agent as #[code --env]: the registry and its credentials, and the private key the
47
+ container clones with. Nothing is set up on the Client host beforehand.
48
+ +code('Reference example').
49
+ # In CI, from its secret store — never typed on the command line:
50
+ export THUB_KEY=… # a CI token (dashboard → CI tokens)
51
+ export DOCKER_PASSWORD=… # the registry password
52
+ export GIT_KEY="$(cat ~/.ssh/thub_deploy)" # a private deploy key with read access to the repository
53
+
54
+ thub run --type sw \
55
+ --env DOCKER_REGISTRY=registry.lab:5000,DOCKER_USERNAME=ci-reader \
56
+ --env DOCKER_PASSWORD --env GIT_KEY \
57
+ --command 'echo "$DOCKER_PASSWORD" | docker login "$DOCKER_REGISTRY" --username "$DOCKER_USERNAME" --password-stdin &&
58
+ mkdir -p "$THUB_WORK_DIR/src" &&
59
+ docker run --rm -e GIT_KEY -e HOME=/tmp --user "$(id -u):$(id -g)" \
60
+ -v "$THUB_WORK_DIR/src:/work" -w /work --entrypoint sh alpine/git -c "
61
+ eval \$(ssh-agent -s) > /dev/null &&
62
+ printf \"%s\n\" \"\$GIT_KEY\" | ssh-add - &&
63
+ GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=accept-new\" git clone --depth 1 git@bitbucket.org:yourorg/web-ui-tests.git . &&
64
+ ./run-tests.sh"' \
65
+ --wait
66
+ ul.small
67
+ li Plain values go as #[code --env NAME=value]. Secrets go as #[code --env NAME] alone: the Agent takes the value from its own environment, so it never appears on the command line, in shell history or in CI logs. A multi-line value, such as the key, arrives intact.
68
+ li The Client sets #[code DOCKER_CONFIG=$THUB_WORK_DIR/.docker]: the registry login lands in the job's directory, deleted with the job. Only #[code src/] is mounted into the container, so the login isn't visible in it.
69
+ li #[code -e GIT_KEY] hands the key to the container. #[code --entrypoint sh] is needed because #[code alpine/git]'s own entrypoint is #[code git]. #[code --user "$(id -u):$(id -g)"] (with #[code HOME=/tmp]) keeps the clone owned by the Client user, so it can be deleted with the job; #[code mkdir -p] first so Docker doesn't create #[code src/] as root.
70
+ li In the inner script, #[code \$] is escaped, so the #[strong container] expands the variables, not the Client's shell. #[code ssh-agent] holds the key in memory: it's never written to a file. #[code accept-new] trusts the git server's host key on first use.
71
+ li #[code alpine/git] comes from Docker Hub. The #[code docker login] is for private images such as #[code $DOCKER_REGISTRY/team/test-runner:1.4].
72
+ li Never #[code echo] a secret: the command's output is the job log, which is stored and visible.
73
+
74
+ h3.h6 An emulator as the DUT
75
+ p.small.
76
+ Start it in the background in the command, talk to it, and stop it whatever the result:
77
+ +code('SW job with an emulator container').
78
+ thub run --type sw \
79
+ --download-file https://artifactory.example.com/fw-local/app/1.4.0-42/app.elf \
80
+ --env DOCKER_REGISTRY=registry.lab.local:5000,DOCKER_USER=ci --env DOCKER_PASSWORD \
81
+ --command 'echo "$DOCKER_PASSWORD" | docker login "$DOCKER_REGISTRY" -u "$DOCKER_USER" --password-stdin &&
82
+ dut="thub-$THUB_JOB_ID" && trap "docker rm -f $dut >/dev/null" EXIT &&
83
+ docker run -d --name "$dut" -p 127.0.0.1:5555:5555 -v "$THUB_DOWNLOADS_DIR:/downloads:ro" \
84
+ "$DOCKER_REGISTRY/dut-emulator:2026.08" &&
85
+ ./ci/test.sh --dut 127.0.0.1:5555' \
86
+ --wait
122
87
  +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 runner card's #[strong Capabilities] tab shows whether a Client can use Docker, and why not.
88
+ Docker access is effectively root access on that host. The service user needs the #[code docker] group: install Docker
89
+ before the Client (#[code sudo npm i -g …] adds the group), or re-run the install afterwards.
@@ -10,17 +10,11 @@
10
10
  const thubVars = [
11
11
  ['THUB_JOB_ID', 'The job id, e.g. M-00125'],
12
12
  ['THUB_ARTIFACTS_FILE', 'Where the command may list the artifacts it published elsewhere: a JSON array of {"name", "size", "link", "timestamp"}, shown on the job page'],
13
- ['THUB_WORK_DIR', 'The directory the command runs in: the --git-repo checkout, else empty. JUnit XML in results/ or artifacts/ gives the test counts; deleted when the job ends'],
14
- ['THUB_SUITE', '--suite (default: default)'],
15
- ['THUB_GIT_COMMIT', 'The exact commit checked out for --git-repo'],
13
+ ['THUB_WORK_DIR', 'The job\'s directory (<client workDir>/<jobId>), where the command starts. Holds downloads/, the artifacts list and .docker/ (the job\'s DOCKER_CONFIG); clone into a folder of your own, e.g. src/. JUnit XML in results/ or artifacts/ gives the test counts; deleted when the job ends'],
14
+ ['DOCKER_CONFIG', '$THUB_WORK_DIR/.docker — so a docker login stays with the job and is deleted with it (unless --env sets DOCKER_CONFIG)'],
16
15
  ['THUB_DOWNLOADS_DIR', 'Where the --download-file files are'],
17
16
  ['THUB_DOWNLOADS / THUB_DOWNLOAD_<n>', 'Local paths of the downloads: all, one per line / each one'],
18
17
  ['THUB_META_<KEY>', '--meta values; camelCase keys become SNAKE_CASE (ciJobId → THUB_META_CI_JOB_ID)'],
19
- ['THUB_DUT_STLINK / _<n>', 'HW: ST-Link serials (device path if the serial couldn\'t be read); unsuffixed = the first'],
20
- ['THUB_DUT_UART / _<n>', 'HW: UART device paths (/dev/thub/dut<N>-uart); unsuffixed = the first'],
21
- ['THUB_DUT_USB / _<n>', 'HW: DUT USB device paths'],
22
- ['THUB_DUT_HOST', 'SW with --docker-image: 127.0.0.1:<port> of the DUT container\'s port 5555'],
23
- ['THUB_DUT_CONTAINER', 'SW with --docker-image: the DUT container\'s name (docker exec / docker logs)']
24
18
  ]
25
19
  .table-responsive
26
20
  table.table.table-sm.small
@@ -38,14 +32,9 @@
38
32
  ['JOB_LABEL / JOB_LABEL_<n>', '--label', 'all labels, comma-separated / each'],
39
33
  ['JOB_GROUP', '(dashboard)', 'the submitting agent\'s group, if it has one'],
40
34
  ['JOB_CLIENT', '--client', 'this Client\'s name'],
41
- ['JOB_USER', '--user', 'as given'],
35
+ ['JOB_USER', '(your key)', 'the submitting user\'s username, or the CI token\'s name'],
42
36
  ['JOB_COMMAND', '--command', 'the command itself'],
43
37
  ['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
38
  ['JOB_SUITE', '--suite', 'as given, default when not'],
50
39
  ['JOB_ARG / JOB_ARG_<n>', '--arg', 'all, space-separated / each (also "$@")'],
51
40
  ['JOB_TIMEOUT', '--timeout', 'seconds'],
@@ -74,8 +63,8 @@
74
63
  ul.small
75
64
  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
65
  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].
66
+ li Any names except #[code THUB_*] and #[code JOB_*].
67
+ li They're how every piece of data and every secret reaches the job: a git token for its #[code git clone], a registry password for its #[code docker login], an API key for its tests.
79
68
  li
80
69
  | Every value is treated as a secret. It's sent only to the Client running the job, shown as #[code ***] by the API,
81
70
  | #[code thub status --json] and #[code --dry-run], and wiped from the Coordinator's database when the job ends.
@@ -88,40 +77,40 @@
88
77
  +code('ci/test.sh (shell)').
89
78
  #!/bin/sh
90
79
  set -eu
91
- echo "Job $THUB_JOB_ID on $(hostname), suite ${THUB_SUITE}, commit ${THUB_GIT_COMMIT:-n/a}"
80
+ echo "Job $THUB_JOB_ID on $(hostname), suite ${JOB_SUITE}"
92
81
 
93
82
  # optional parameters with defaults
94
83
  TARGET="${TARGET:-staging}"
95
- BRANCH="${JOB_GIT_BRANCH:-main}"
84
+ BRANCH="${BRANCH:-main}" # from --env BRANCH=…
96
85
 
97
86
  # every downloaded file
98
87
  printf '%s\n' "$THUB_DOWNLOADS" | while read -r f; do echo "downloaded: $f"; done
99
88
 
100
89
  # 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
90
+ st-flash --reset write "$THUB_DOWNLOAD_1" 0x08000000
91
+ python3 -m pytest tests/ --uart "/dev/thub/dut2-uart" --junitxml=results/junit.xml
103
92
 
104
93
  # CI metadata passed with --meta ciJobId=… / --meta sha=…
105
94
  echo "CI run ${THUB_META_CI_JOB_ID:-local}, sha ${THUB_META_SHA:-unknown}"
106
95
  +code('conftest.py (Python)').
107
- import os
96
+ import glob, os
108
97
 
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_")]
98
+ UARTS = sorted(glob.glob("/dev/thub/dut*-uart")) # this Client's UARTs, by udev path
111
99
  FIRMWARE = os.environ.get("THUB_DOWNLOAD_1")
112
- RESULTS = os.path.join(os.environ.get("THUB_WORK_DIR", "."), "results")
100
+ RESULTS = "results" # in the clone (src/results), where the tests run
113
101
  API_TOKEN = os.environ["API_TOKEN"] # from --env API_TOKEN
114
102
  +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 \
103
+ docker run --rm --user "$(id -u):$(id -g)" -v "$THUB_WORK_DIR/src:/work" -w /work \
104
+ -e THUB_JOB_ID -e JOB_SUITE -e TARGET -e API_TOKEN \
117
105
  python:3.14 ./run-tests.sh
118
106
  +code('Submitting the job that feeds these').
119
107
  export API_TOKEN=…
120
108
  thub run --type hw --label board:nucleo-f401re \
121
109
  --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 \
110
+ --env TARGET=production,BRANCH=main --env API_TOKEN --env GH_TOKEN \
124
111
  --meta ciJobId="$GITHUB_RUN_ID" --meta sha="$GITHUB_SHA" \
125
- --suite smoke --command ./ci/test.sh --wait
112
+ --suite smoke \
113
+ --command 'git clone --depth 1 --branch "$BRANCH" "https://x-access-token:$GH_TOKEN@github.com/yourorg/firmware-tests.git" src && cd src &&
114
+ ./ci/test.sh' --wait
126
115
  p.small.text-body-secondary.mb-0.
127
116
  Tip: #[code thub run … --dry-run] lists every variable the command would get on the chosen Client, with #[code --env] values as #[code ***].