@andrian.yablonskyy/thub-coordinator 1.1.12 → 1.1.13
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/package.json +2 -2
- package/public/css/thub.css +72 -0
- package/public/js/tag-picker.js +200 -0
- package/src/dev/virtual.js +1 -3
- package/src/services/jobs.js +14 -1
- package/test/groups-tab.test.js +4 -2
- package/test/list-prefs.test.js +7 -4
- package/test/scheduler.test.js +6 -2
- package/test/search.test.js +2 -3
- package/test/users.test.js +14 -0
- package/views/admin/agents.pug +21 -17
- package/views/admin/users.pug +43 -40
- package/views/groups/list.pug +20 -16
- package/views/help/_agent-cli.pug +11 -15
- package/views/help/_agent-setup.pug +0 -4
- package/views/help/_ci.pug +3 -2
- package/views/help/_client-machines.pug +12 -11
- package/views/help/_client-setup.pug +4 -4
- package/views/help/_docker.pug +29 -92
- package/views/help/_env.pug +11 -19
- package/views/help/_git.pug +36 -123
- package/views/help/_overview.pug +2 -2
- package/views/help/_quick-start.pug +1 -1
- package/views/help/_troubleshooting.pug +3 -1
- package/views/help/index.pug +2 -2
- package/views/index.pug +1 -0
- package/views/mixins/client-config.pug +29 -13
- package/views/mixins/resource.pug +2 -2
- package/views/resources/list.pug +1 -0
|
@@ -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
28
|
['--suite <name>', 'Passed as THUB_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
|
-
['--
|
|
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
|
-
--
|
|
78
|
-
--command '
|
|
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" . &&
|
|
74
|
+
st-flash --serial "$THUB_DUT_STLINK" --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
|
|
81
|
-
thub run --type hw --label board:nucleo-f401re --client lab-hw-01
|
|
82
|
-
--download-file "$IMAGE_URL" --git
|
|
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" . && ./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
|
|
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 --
|
|
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 --
|
|
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
|
package/views/help/_ci.pug
CHANGED
|
@@ -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
|
-
--
|
|
37
|
-
--command '
|
|
36
|
+
--env GH_TOKEN="${{ secrets.TESTS_READ_TOKEN }}",SHA="$GITHUB_SHA",REPO="$GITHUB_REPOSITORY" \
|
|
37
|
+
--command 'git init -q . && git fetch -q --depth 1 "https://x-access-token:$GH_TOKEN@github.com/$REPO.git" "$SHA" && git checkout -q FETCH_HEAD &&
|
|
38
|
+
st-flash --serial "$THUB_DUT_STLINK" --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]
|
|
30
|
-
li
|
|
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
|
-
--
|
|
56
|
-
--command '
|
|
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" . &&
|
|
57
|
+
st-flash --serial "$THUB_DUT_STLINK" --reset write "$THUB_DOWNLOAD_1" 0x08000000 && ./ci/test.sh' \
|
|
57
58
|
--wait
|
|
58
59
|
|
|
59
60
|
+note('warning').
|
|
@@ -62,26 +63,26 @@
|
|
|
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
|
-
--
|
|
78
|
-
--git-
|
|
79
|
-
|
|
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" . &&
|
|
78
|
+
export DOCKER_CONFIG="$THUB_WORK_DIR/.docker" &&
|
|
79
|
+
echo "$DOCKER_PASSWORD" | docker login registry.lab.local:5000 -u ci --password-stdin &&
|
|
80
|
+
docker run --rm -v "$PWD:/work" -w /work registry.lab.local:5000/dut-emulator:2026.08 make test' --wait
|
|
80
81
|
|
|
81
82
|
h3.h6 Checklist for any Client machine
|
|
82
83
|
ul.small.mb-0
|
|
83
84
|
li Correct time (NTP). Timestamps are anchored to the Coordinator's clock, but TLS needs a sane clock.
|
|
84
85
|
li Outbound HTTPS to the Coordinator; test it with #[code curl -sI #{coordinatorUrl}/login].
|
|
85
|
-
li
|
|
86
|
+
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
87
|
li Under systemd, the service can only write to its state directory. Jobs write to #[code $THUB_WORK_DIR], not to #[code ~].
|
|
87
88
|
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
|
-
#
|
|
12
|
-
#
|
|
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] (
|
|
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,7 +100,7 @@
|
|
|
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:
|
|
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].
|
package/views/help/_docker.pug
CHANGED
|
@@ -1,99 +1,24 @@
|
|
|
1
|
-
+section('docker', 'Docker
|
|
1
|
+
+section('docker', 'Using Docker in a job', 'box-seam')
|
|
2
2
|
p.
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
.
|
|
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
|
|
7
|
+
h3.h6 Log in, pull and run, credentials scoped to the job
|
|
23
8
|
p.small.
|
|
24
|
-
#[code
|
|
25
|
-
|
|
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:
|
|
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
16
|
--command 'export DOCKER_CONFIG="$THUB_WORK_DIR/.docker" &&
|
|
91
17
|
echo "$DOCKER_PASSWORD" | docker login "$DOCKER_REGISTRY" -u "$DOCKER_USER" --password-stdin &&
|
|
92
18
|
docker run --rm \
|
|
93
|
-
-v "$THUB_WORK_DIR:/work" -w /work \
|
|
19
|
+
-v "$THUB_WORK_DIR:/work" -w /work --user "$(id -u):$(id -g)" \
|
|
94
20
|
-e TARGET -e THUB_SUITE -e THUB_JOB_ID \
|
|
95
|
-
--
|
|
96
|
-
"$DOCKER_REGISTRY/python:3.14" ./run-tests.sh --junit results/junit.xml' \
|
|
21
|
+
"$DOCKER_REGISTRY/team/test-runner:1.4" ./run-tests.sh --junit results/junit.xml' \
|
|
97
22
|
--wait
|
|
98
23
|
table.table.table-sm.small
|
|
99
24
|
thead
|
|
@@ -106,19 +31,31 @@
|
|
|
106
31
|
td Removes the container when the tests end.
|
|
107
32
|
tr
|
|
108
33
|
td: code -v "$THUB_WORK_DIR:/work" -w /work
|
|
109
|
-
td The
|
|
34
|
+
td The work directory, where the Client reads JUnit XML under #[code results/] and #[code artifacts/] for the test counts.
|
|
110
35
|
tr
|
|
111
36
|
td: code -e NAME
|
|
112
37
|
td Passes a job variable (#[code --env], #[code THUB_*], #[code JOB_*]) into the container.
|
|
113
38
|
tr
|
|
114
|
-
td: code --
|
|
115
|
-
td
|
|
39
|
+
td: code --user "$(id -u):$(id -g)"
|
|
40
|
+
td Files written to #[code /work] stay owned by the Client user, so the workspace can be cleaned up.
|
|
116
41
|
tr
|
|
117
42
|
td: code --device /dev/thub/dut1-uart
|
|
118
43
|
td HW: gives the container the DUT's UART (#[code "$THUB_DUT_UART"]). Use #[code --privileged] only if you must.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
44
|
+
|
|
45
|
+
h3.h6 An emulator as the DUT
|
|
46
|
+
p.small.
|
|
47
|
+
Start it in the background in the command, talk to it, and stop it whatever the result:
|
|
48
|
+
+code('SW job with an emulator container').
|
|
49
|
+
thub run --type sw \
|
|
50
|
+
--download-file https://artifactory.example.com/fw-local/app/1.4.0-42/app.elf \
|
|
51
|
+
--env DOCKER_REGISTRY=registry.lab.local:5000,DOCKER_USER=ci --env DOCKER_PASSWORD \
|
|
52
|
+
--command 'export DOCKER_CONFIG="$THUB_WORK_DIR/.docker" &&
|
|
53
|
+
echo "$DOCKER_PASSWORD" | docker login "$DOCKER_REGISTRY" -u "$DOCKER_USER" --password-stdin &&
|
|
54
|
+
dut="thub-$THUB_JOB_ID" && trap "docker rm -f $dut >/dev/null" EXIT &&
|
|
55
|
+
docker run -d --name "$dut" -p 127.0.0.1:5555:5555 -v "$THUB_DOWNLOADS_DIR:/downloads:ro" \
|
|
56
|
+
"$DOCKER_REGISTRY/dut-emulator:2026.08" &&
|
|
57
|
+
./ci/test.sh --dut 127.0.0.1:5555' \
|
|
58
|
+
--wait
|
|
122
59
|
+note('info').
|
|
123
|
-
Docker access is effectively root access on that host. The
|
|
124
|
-
#[code
|
|
60
|
+
Docker access is effectively root access on that host. The service user needs the #[code docker] group: install Docker
|
|
61
|
+
before the Client (#[code sudo npm i -g …] adds the group), or re-run the install afterwards.
|
package/views/help/_env.pug
CHANGED
|
@@ -10,17 +10,14 @@
|
|
|
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
|
|
13
|
+
['THUB_WORK_DIR', 'The directory the command runs in, empty at the start (clone into it if you need a repository). JUnit XML in results/ or artifacts/ gives the test counts; deleted when the job ends'],
|
|
14
14
|
['THUB_SUITE', '--suite (default: default)'],
|
|
15
|
-
['THUB_GIT_COMMIT', 'The exact commit checked out for --git-repo'],
|
|
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
18
|
['THUB_DUT_STLINK / _<n>', 'HW: ST-Link serials (device path if the serial couldn\'t be read); unsuffixed = the first'],
|
|
20
19
|
['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)']
|
|
20
|
+
['THUB_DUT_USB / _<n>', 'HW: DUT USB device paths']
|
|
24
21
|
]
|
|
25
22
|
.table-responsive
|
|
26
23
|
table.table.table-sm.small
|
|
@@ -38,14 +35,9 @@
|
|
|
38
35
|
['JOB_LABEL / JOB_LABEL_<n>', '--label', 'all labels, comma-separated / each'],
|
|
39
36
|
['JOB_GROUP', '(dashboard)', 'the submitting agent\'s group, if it has one'],
|
|
40
37
|
['JOB_CLIENT', '--client', 'this Client\'s name'],
|
|
41
|
-
['JOB_USER', '
|
|
38
|
+
['JOB_USER', '(your key)', 'the submitting user\'s username, or the CI token\'s name'],
|
|
42
39
|
['JOB_COMMAND', '--command', 'the command itself'],
|
|
43
40
|
['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
41
|
['JOB_SUITE', '--suite', 'as given, default when not'],
|
|
50
42
|
['JOB_ARG / JOB_ARG_<n>', '--arg', 'all, space-separated / each (also "$@")'],
|
|
51
43
|
['JOB_TIMEOUT', '--timeout', 'seconds'],
|
|
@@ -74,8 +66,8 @@
|
|
|
74
66
|
ul.small
|
|
75
67
|
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
68
|
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_*]
|
|
78
|
-
li They
|
|
69
|
+
li Any names except #[code THUB_*] and #[code JOB_*].
|
|
70
|
+
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
71
|
li
|
|
80
72
|
| Every value is treated as a secret. It's sent only to the Client running the job, shown as #[code ***] by the API,
|
|
81
73
|
| #[code thub status --json] and #[code --dry-run], and wiped from the Coordinator's database when the job ends.
|
|
@@ -88,11 +80,11 @@
|
|
|
88
80
|
+code('ci/test.sh (shell)').
|
|
89
81
|
#!/bin/sh
|
|
90
82
|
set -eu
|
|
91
|
-
echo "Job $THUB_JOB_ID on $(hostname), suite ${THUB_SUITE}
|
|
83
|
+
echo "Job $THUB_JOB_ID on $(hostname), suite ${THUB_SUITE}"
|
|
92
84
|
|
|
93
85
|
# optional parameters with defaults
|
|
94
86
|
TARGET="${TARGET:-staging}"
|
|
95
|
-
BRANCH="${
|
|
87
|
+
BRANCH="${BRANCH:-main}" # from --env BRANCH=…
|
|
96
88
|
|
|
97
89
|
# every downloaded file
|
|
98
90
|
printf '%s\n' "$THUB_DOWNLOADS" | while read -r f; do echo "downloaded: $f"; done
|
|
@@ -106,7 +98,6 @@
|
|
|
106
98
|
+code('conftest.py (Python)').
|
|
107
99
|
import os
|
|
108
100
|
|
|
109
|
-
DUT_HOST = os.environ.get("THUB_DUT_HOST", "127.0.0.1:5555") # SW DUT container
|
|
110
101
|
UARTS = [v for k, v in sorted(os.environ.items()) if k.startswith("THUB_DUT_UART_")]
|
|
111
102
|
FIRMWARE = os.environ.get("THUB_DOWNLOAD_1")
|
|
112
103
|
RESULTS = os.path.join(os.environ.get("THUB_WORK_DIR", "."), "results")
|
|
@@ -119,9 +110,10 @@
|
|
|
119
110
|
export API_TOKEN=…
|
|
120
111
|
thub run --type hw --label board:nucleo-f401re \
|
|
121
112
|
--download-file https://artifactory.example.com/fw-local/app/1.4.0-42/app.bin \
|
|
122
|
-
--
|
|
123
|
-
--env TARGET=production --env API_TOKEN \
|
|
113
|
+
--env TARGET=production,BRANCH=main --env API_TOKEN --env GH_TOKEN \
|
|
124
114
|
--meta ciJobId="$GITHUB_RUN_ID" --meta sha="$GITHUB_SHA" \
|
|
125
|
-
--suite smoke
|
|
115
|
+
--suite smoke \
|
|
116
|
+
--command 'git clone --depth 1 --branch "$BRANCH" "https://x-access-token:$GH_TOKEN@github.com/yourorg/firmware-tests.git" . &&
|
|
117
|
+
./ci/test.sh' --wait
|
|
126
118
|
p.small.text-body-secondary.mb-0.
|
|
127
119
|
Tip: #[code thub run … --dry-run] lists every variable the command would get on the chosen Client, with #[code --env] values as #[code ***].
|
package/views/help/_git.pug
CHANGED
|
@@ -1,130 +1,43 @@
|
|
|
1
|
-
+section('git', '
|
|
1
|
+
+section('git', 'Using git in a job', 'git')
|
|
2
2
|
p.
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
The Client doesn't clone anything itself and doesn't need git installed for its own sake. A job that needs a repository
|
|
4
|
+
clones it in #[code --command], into its work directory (#[code $THUB_WORK_DIR], where the command starts), with the
|
|
5
|
+
credentials passed as #[code --env]. #[code --env] values are masked everywhere and dropped when the job ends. Anything
|
|
6
|
+
else (the URL, the command, #[code --meta]) is stored with the job and visible to anyone who can read it, so never put a
|
|
7
|
+
token there.
|
|
6
8
|
|
|
7
|
-
h3.h6
|
|
8
|
-
+code('
|
|
9
|
-
|
|
10
|
-
mkdir -p ci
|
|
11
|
-
cat > ci/test.sh <<'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 summed into
|
|
24
|
-
the job's test counts. Nothing is uploaded: the workspace is deleted when the job ends, so publish anything you need to keep
|
|
25
|
-
from the command itself (e.g. #[code curl -T report.html "$ARTIFACTORY/…"] with a token passed as #[code --env]).
|
|
26
|
-
|
|
27
|
-
h3.h6 2. Give Clients read access: SSH deploy key (recommended)
|
|
28
|
-
p.small.
|
|
29
|
-
git never prompts on a Client (#[code GIT_TERMINAL_PROMPT=0]), so credentials must already be on the Client host,
|
|
30
|
-
owned by the #[strong Client's service user]. Create a key on each Client host (or one shared key per lab):
|
|
31
|
-
+code('Client host, as the Client\'s user').
|
|
32
|
-
ssh-keygen -t ed25519 -N "" -C "thub-client@$(hostname)" -f ~/.ssh/thub_deploy
|
|
33
|
-
cat ~/.ssh/thub_deploy.pub # → add as a read-only deploy key (below)
|
|
34
|
-
# trust the git server's host key now: the service can't answer prompts
|
|
35
|
-
# (and can't write ~/.ssh under systemd)
|
|
36
|
-
ssh-keyscan github.com bitbucket.org gitlab.com >> ~/.ssh/known_hosts
|
|
37
|
-
# use the key for that server
|
|
38
|
-
cat >> ~/.ssh/config <<'EOF'
|
|
39
|
-
Host github.com
|
|
40
|
-
IdentityFile ~/.ssh/thub_deploy
|
|
41
|
-
IdentitiesOnly yes
|
|
42
|
-
EOF
|
|
43
|
-
chmod 600 ~/.ssh/config
|
|
44
|
-
ssh -T git@github.com # "…successfully authenticated…"
|
|
45
|
-
table.table.table-sm.small
|
|
46
|
-
thead
|
|
47
|
-
tr
|
|
48
|
-
th Git server
|
|
49
|
-
th Where to add the public key
|
|
50
|
-
tbody
|
|
51
|
-
tr
|
|
52
|
-
td GitHub
|
|
53
|
-
td Repository → Settings → Deploy keys → Add deploy key (leave “Allow write access” off). For many repositories, use a machine user's SSH key instead.
|
|
54
|
-
tr
|
|
55
|
-
td GitLab
|
|
56
|
-
td Project → Settings → Repository → Deploy keys.
|
|
57
|
-
tr
|
|
58
|
-
td Bitbucket
|
|
59
|
-
td Repository settings → Security → Access keys.
|
|
60
|
-
tr
|
|
61
|
-
td Gitea / self-hosted
|
|
62
|
-
td Repository → Settings → Deploy Keys, or the machine user's SSH keys.
|
|
63
|
-
p.small.
|
|
64
|
-
Without #[code ~/.ssh/config], pick the key per job with #[code --git-options] (stored with the job, so reference a key
|
|
65
|
-
#[em file], never inline a secret):
|
|
66
|
-
+code('Per-job SSH key / port').
|
|
9
|
+
h3.h6 HTTPS with a token (recommended)
|
|
10
|
+
+code('Token from your CI secret store').
|
|
11
|
+
export GH_TOKEN=… # GitHub: a fine-grained PAT with read access; GitLab: oauth2 token; Bitbucket: x-token-auth
|
|
67
12
|
thub run --type sw \
|
|
68
|
-
--
|
|
69
|
-
--
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
h3.h6 HTTPS with a token
|
|
73
|
-
p.small.
|
|
74
|
-
For #[code https://] repositories, either store a credential helper for the Client's user on the host
|
|
75
|
-
(#[code git config --global credential.helper store]), or pass the token #[strong per job] as a secret. #[code --env]
|
|
76
|
-
variables reach the Client's own git commands, and git reads config from #[code GIT_CONFIG_COUNT]/#[code _KEY_n]/#[code _VALUE_n]:
|
|
77
|
-
+code('Token per job, never stored').
|
|
78
|
-
# GitHub: user x-access-token; GitLab: oauth2; Bitbucket: x-token-auth
|
|
79
|
-
export GIT_CONFIG_VALUE_0="Authorization: Basic $(printf 'x-access-token:%s' "$GH_TOKEN" | base64 | tr -d '\n')"
|
|
80
|
-
thub run --type sw \
|
|
81
|
-
--git-repo https://github.com/yourorg/private-tests.git main \
|
|
82
|
-
--env GIT_CONFIG_COUNT=1,GIT_CONFIG_KEY_0=http.extraHeader --env GIT_CONFIG_VALUE_0 \
|
|
83
|
-
--command ./ci/test.sh --wait
|
|
13
|
+
--env GH_TOKEN \
|
|
14
|
+
--command 'git clone --depth 1 --branch main "https://x-access-token:$GH_TOKEN@github.com/yourorg/firmware-tests.git" . &&
|
|
15
|
+
./ci/test.sh' \
|
|
16
|
+
--wait
|
|
84
17
|
p.small.
|
|
85
|
-
|
|
86
|
-
|
|
18
|
+
The token appears only inside the command's environment. To keep it out of #[code .git/config] as well, pass it as a header:
|
|
19
|
+
#[code git -c http.extraHeader="Authorization: Bearer $GH_TOKEN" clone …].
|
|
87
20
|
|
|
88
|
-
h3.h6
|
|
89
|
-
+code('
|
|
90
|
-
|
|
91
|
-
--
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
tr
|
|
102
|
-
th Default ref
|
|
103
|
-
td The repository's default branch.
|
|
104
|
-
tr
|
|
105
|
-
th Depth
|
|
106
|
-
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.
|
|
107
|
-
tr
|
|
108
|
-
th Exact commit
|
|
109
|
-
td Logged, and passed to the command as #[code $THUB_GIT_COMMIT]. The requested ref is #[code $JOB_GIT_BRANCH].
|
|
110
|
-
tr
|
|
111
|
-
th Extra git options
|
|
112
|
-
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="…"'].
|
|
113
|
-
+code('What the Client runs (see it with --dry-run)').
|
|
114
|
-
git -C <work> init -q
|
|
115
|
-
git -C <work> remote add origin <url>
|
|
116
|
-
git -C <work> fetch -q --depth 1 origin <ref>
|
|
117
|
-
git -C <work> checkout -q --detach FETCH_HEAD
|
|
118
|
-
git -C <work> rev-parse HEAD
|
|
21
|
+
h3.h6 SSH with a key from the job
|
|
22
|
+
+code('Private key as --env (base64), used for this job only').
|
|
23
|
+
export DEPLOY_KEY_B64="$(base64 < ~/.ssh/thub_deploy | tr -d '\n')"
|
|
24
|
+
thub run --type hw --label board:nucleo-f401re \
|
|
25
|
+
--env DEPLOY_KEY_B64 \
|
|
26
|
+
--command 'umask 077 && echo "$DEPLOY_KEY_B64" | base64 -d > "$THUB_WORK_DIR/../key" &&
|
|
27
|
+
GIT_SSH_COMMAND="ssh -i $THUB_WORK_DIR/../key -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new" \
|
|
28
|
+
git clone --depth 1 git@github.com:yourorg/firmware-tests.git . &&
|
|
29
|
+
./ci/test.sh' \
|
|
30
|
+
--wait
|
|
31
|
+
p.small.
|
|
32
|
+
The job directory, key included, is deleted when the job ends. Add the public key as a read-only deploy key: GitHub,
|
|
33
|
+
Repository → Settings → Deploy keys; GitLab, Settings → Repository → Deploy keys; Bitbucket, Repository settings → Access keys.
|
|
119
34
|
|
|
120
|
-
h3.h6
|
|
35
|
+
h3.h6 Branch, tag, commit, submodules, LFS
|
|
36
|
+
+code('All in the command').
|
|
37
|
+
git clone --depth 1 --branch v1.4.0 "$REPO" . # a branch or tag
|
|
38
|
+
git init -q . && git remote add origin "$REPO" && git fetch -q --depth 1 origin a1b2c3d && git checkout -q FETCH_HEAD # a commit
|
|
39
|
+
git submodule update --init --recursive --depth 1 && git lfs pull # submodules / LFS
|
|
121
40
|
p.small.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
--command 'git submodule update --init --recursive --depth 1 && git lfs pull && ./ci/test.sh' --wait
|
|
126
|
-
+code('Clone yourself, e.g. inside a container (JOB_GIT_* carry the --git-repo values)').
|
|
127
|
-
thub run --type sw --git-repo git@github.com:yourorg/tests.git main --depth 1 \
|
|
128
|
-
--command 'docker run --rm -v "$HOME/.ssh:/root/.ssh:ro" -e JOB_GIT_REPO_URL -e JOB_GIT_BRANCH -e JOB_GIT_DEPTH \
|
|
129
|
-
--entrypoint sh alpine/git -c "git clone --depth \$JOB_GIT_DEPTH --branch \$JOB_GIT_BRANCH \$JOB_GIT_REPO_URL /src && ls /src"' \
|
|
130
|
-
--wait
|
|
41
|
+
Pass the repository and ref as #[code --env REPO=…,REF=…] (or #[code --meta]) so the same command works for every branch.
|
|
42
|
+
The exit code of #[code --command] is the verdict; JUnit files under #[code results/] or #[code artifacts/] are summed into
|
|
43
|
+
the job's test counts. Nothing is uploaded: publish what you need to keep from the command (#[code curl -T …] with a token in #[code --env]).
|
package/views/help/_overview.pug
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
ol.small
|
|
40
40
|
li #[code thub run …] submits a job spec (type, labels, command, inputs) with your access key (or a pipeline's CI token).
|
|
41
41
|
li The Coordinator queues it (#[code QUEUED]) and the scheduler assigns it to an idle Client whose type, labels and group match.
|
|
42
|
-
li The Client accepts it,
|
|
42
|
+
li The Client accepts it, downloads #[code --download-file] files and prepares the DUT (#[code PREPARING]).
|
|
43
43
|
li It runs #[code --command] with #[code sh -c], and output streams live to the Agent and the dashboard (#[code RUNNING]).
|
|
44
44
|
li The exit code is the verdict: #[code 0] = #[code PASSED], anything else = #[code FAILED]. The Client reports the JUnit test counts; the job's files stay on the Client and are deleted with its workspace.
|
|
45
45
|
li The Agent exits with the verdict code, so a CI step passes or fails with it.
|
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
-
|
|
103
103
|
const useCases = [
|
|
104
104
|
{ icon: 'cpu', title: 'Hardware-in-the-loop CI', text: 'Every push builds firmware in the cloud, then a GitHub Actions test job runs `thub run --type hw --wait`: a lab Client flashes a real board through ST-Link, captures its UART, runs the tests and fails the pipeline on a non-zero exit.' },
|
|
105
|
-
{ icon: 'pc-display', title: 'Emulator (SW) test farms', text: 'SW Clients on real or virtual Linux machines run each job’s
|
|
105
|
+
{ icon: 'pc-display', title: 'Emulator (SW) test farms', text: 'SW Clients on real or virtual Linux machines run each job’s command, which starts its emulator (QEMU, Renode, a simulator) — in Docker if it likes — and tests it. Scale out by adding VMs.' },
|
|
106
106
|
{ icon: 'person-workspace', title: 'Remote access for developers', text: 'Developers reproduce a CI failure on the exact bench it happened on (`--client lab-hw-01`) from home, with the same command CI ran, and stream the board’s console live.' },
|
|
107
107
|
{ icon: 'lock', title: 'Shared benches without collisions', text: 'The scheduler hands out one job per bench. An engineer who needs a board by hand runs `thub-client lock` so nothing is scheduled there until `unlock`.' },
|
|
108
108
|
{ icon: 'collection', title: 'Dedicated pools', text: 'Groups and labels (`board:nucleo-f401re`, `uart`) route jobs to the right hardware, e.g. a nightly pool separate from the PR pool.' },
|