@andrian.yablonskyy/thub-coordinator 1.1.11 → 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.
@@ -1,130 +1,43 @@
1
- +section('git', 'Git repository integration', 'git')
1
+ +section('git', 'Using git in a job', 'git')
2
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.
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 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 <<'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
- --git-repo ssh://git@git.lab.local:2222/qa/tests.git main \
69
- --git-options '-c core.sshCommand="ssh -i ~/.ssh/thub_deploy -o IdentitiesOnly=yes"' \
70
- --command ./ci/test.sh --wait
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
- Don't put a token in the URL, #[code --git-options], #[code --command] or #[code --meta]: those are stored with the job
86
- and visible to anyone who can read it. Only #[code --env] values are masked.
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 3. Clone options: ref, branch, tag, commit, depth
89
- +code('--git-repo <url> [<branch>|<tag>|<commit>] [--depth <n>]').
90
- --git-repo git@github.com:yourorg/tests.git # default branch, depth 1
91
- --git-repo git@github.com:yourorg/tests.git develop # a branch
92
- --git-repo git@github.com:yourorg/tests.git v1.4.0 # a tag
93
- --git-repo git@github.com:yourorg/tests.git a1b2c3d # a commit (may be abbreviated)
94
- --git-repo git@github.com:yourorg/tests.git main --depth 50 # last 50 commits
95
- --git-repo git@github.com:yourorg/tests.git main --depth 0 # full history (git describe, changelogs)
96
- table.table.table-sm.small
97
- tbody
98
- tr
99
- th Transports
100
- td #[code https://], #[code http://], #[code ssh://], #[code git://], #[code user@host:path]. #[code file://] and #[code ext::] are refused.
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 &lt;work> init -q
115
- git -C &lt;work> remote add origin &lt;url>
116
- git -C &lt;work> fetch -q --depth 1 origin &lt;ref>
117
- git -C &lt;work> checkout -q --detach FETCH_HEAD
118
- git -C &lt;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 &lt; ~/.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 Submodules, LFS and cloning inside a container
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
- 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:
123
- +code('Submodules / LFS').
124
- thub run --type sw --git-repo git@github.com:yourorg/tests.git main \
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]).
@@ -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, clones #[code --git-repo], downloads #[code --download-file] files and prepares the DUT (#[code PREPARING]).
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 DUT image (QEMU, Renode, a simulator) in a sandboxed Docker container. The tests talk to it at $THUB_DUT_HOST. Scale out by adding VMs.' },
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.' },
@@ -14,7 +14,7 @@
14
14
  strong Install a Client
15
15
  | on each lab machine, with the same join key (#[a(href="#client-setup") Client setup]):
16
16
  +code('Lab machine (Ubuntu 26.04)').
17
- sudo apt install -y nodejs npm docker.io # + stlink-tools openocd for HW
17
+ sudo apt install -y nodejs npm # + your jobs' tools: stlink-tools openocd, git, docker.io…
18
18
  sudo npm i -g @andrian.yablonskyy/thub-client
19
19
  nano ~/.config/thub/client.json # coordinatorUrl, type, joinKey
20
20
  sudo systemctl enable --now thub-client@client
@@ -8,7 +8,9 @@
8
8
  ['Name already in use (`409`)', 'Another live Client holds that name. Rename one of them in its config or from the runner card. A name held by an `OUT_OF_SERVICE` runner is reclaimed automatically.'],
9
9
  ['The job is rejected with `422`', 'No registered Client can ever match its type, labels, group or client. Compare `thub resources` with your `--type` and `--label`s; your group (set on the dashboard, see `thub whoami`) must have a Client of that kind.'],
10
10
  ['The job stays `QUEUED`', 'Matching Clients are all `BUSY`, locked (`local`), in `MAINTENANCE` or offline, or the job is pinned with `--client`. It times out after `--timeout`.'],
11
- ['`ERROR` during prepare', 'The clone, download or DUT image pull failed; the job log names the reason. For git, check the Client user\'s keys and `known_hosts` (`ssh -T git@<host>` as that user). For images, check the Client user\'s `docker login`.'],
11
+ ['`ERROR` during prepare', 'A download failed; the job log names the URL and the reason.'],
12
+ ['A git clone or docker pull in the command fails', 'They run in your `--command`, with the credentials you pass as `--env` (see Using git / Using Docker). `git` or `docker` must be installed on that Client host; the Client itself needs neither. Try the command with `--dry-run` to see its environment.'],
13
+ ['`--git-repo` / `--docker-image`: unknown option, or the job is refused', 'They were removed: clone the repository and run containers in `--command`, passing tokens with `--env`. An older Agent that still sends them is refused with that message; update it (`thub self-update`).'],
12
14
  ['Permission denied on `/dev/ttyUSB*` or `docker.sock`', 'Docker or the device groups were added after the Client was installed. Re-run `sudo npm i -g @andrian.yablonskyy/thub-client` so the unit gets the `dialout`/`plugdev`/`docker` groups, then restart.'],
13
15
  ['A configured device is shown as missing', 'Check the `devpath` with `udevadm info -a -n <dev>`, then `thub-client udev --print`; restart the Client to regenerate the udev rules.'],
14
16
  ['Job `LOST`', 'The Client missed 3 heartbeats (network, reboot, crash). With `requeueOnLost` the job is retried once on another Client.'],
@@ -11,8 +11,8 @@ block content
11
11
  { id: 'agent-setup', label: 'Agent setup' },
12
12
  { id: 'client-setup', label: 'Client setup' },
13
13
  { id: 'client-machines', label: 'Client machines (HW / SW)' },
14
- { id: 'docker', label: 'Docker registry' },
15
- { id: 'git', label: 'Git repositories' },
14
+ { id: 'docker', label: 'Using Docker' },
15
+ { id: 'git', label: 'Using git' },
16
16
  { id: 'agent-cli', label: 'Agent CLI reference' },
17
17
  { id: 'env', label: 'Environment variables' },
18
18
  { id: 'ci', label: 'CI/CD (GitHub Actions)' },
package/views/index.pug CHANGED
@@ -49,6 +49,7 @@ block scripts
49
49
  script(src="/js/resource-card.js")
50
50
  script(src="/js/remove-resource.js")
51
51
  script(src="/js/client-config.js")
52
+ script(src="/js/tag-picker.js")
52
53
  script(src="/js/usb-scan.js")
53
54
  script(src="/js/usb-parse.js")
54
55
  script(src="/js/usb-import.js")
@@ -183,19 +183,35 @@ mixin groupsPane(r, returnTo, groupsById)
183
183
  data-confirm-tone="primary"
184
184
  )
185
185
  input(type="hidden" name="returnTo" value=returnTo)
186
- fieldset(disabled=!canEdit || r.config_support !== 'file')
187
- .list-group.mb-2
188
- each g in all
189
- label.list-group-item.d-flex.gap-2.align-items-start
190
- input.form-check-input.flex-shrink-0.mt-1(type="checkbox" name="groups" value=g.id checked=r.group_ids.includes(g.id))
191
- span
192
- span.fw-medium= g.name
193
- if g.comment
194
- span.small.text-body-secondary= ` — ${g.comment}`
195
- each id in unknown
196
- label.list-group-item.d-flex.gap-2.align-items-start
197
- input.form-check-input.flex-shrink-0.mt-1(type="checkbox" name="groups" value=id checked)
198
- span.text-body-secondary (a deleted group)
186
+ - const editable = canEdit && r.config_support === 'file'
187
+ fieldset(disabled=!editable)
188
+ //- Chips for its groups, a search box to add more (public/js/tag-picker.js):
189
+ //- each chip carries its hidden `groups` input; ✕ removes it.
190
+ .thub-tags.mb-2(data-tag-picker data-tag-name="groups" data-options=JSON.stringify(all.map((g) => ({ value: g.id, label: g.name, hint: g.comment || '' }))))
191
+ .thub-tags-box.form-control(data-tag-box)
192
+ each id in r.group_ids
193
+ span.thub-tag(data-tag=id)
194
+ span= groupsById[id] ? groupsById[id].name : '(a deleted group)'
195
+ input(type="hidden" name="groups" value=id)
196
+ if editable
197
+ button.thub-tag-remove(type="button" data-tag-remove aria-label=`Remove ${groupsById[id] ? groupsById[id].name : 'deleted group'}`)
198
+ i.bi.bi-x
199
+ if editable
200
+ input.thub-tags-input(
201
+ type="text"
202
+ data-tag-input
203
+ placeholder="Add a group…"
204
+ aria-label=`Add a group to ${r.name}`
205
+ role="combobox"
206
+ aria-autocomplete="list"
207
+ aria-expanded="false"
208
+ aria-controls=`${p}-groups-menu`
209
+ autocomplete="off"
210
+ spellcheck="false"
211
+ )
212
+ ul.dropdown-menu.thub-tags-menu(id=`${p}-groups-menu` role="listbox" data-tag-menu)
213
+ if !r.group_ids.length && !editable
214
+ span.small.text-body-secondary none
199
215
  .form-text An agent given a group (on Users or CI tokens) runs its jobs only on Clients in it; an agent without one, on any Client.
200
216
  if canEdit && r.config_support === 'file'
201
217
  .d-flex.justify-content-end.mt-3
@@ -135,12 +135,12 @@ mixin capDevice(d, detail)
135
135
  ) missing
136
136
 
137
137
  //- What the Client reported it can drive at registration (HW: udev
138
- //- devices; SW: nothing of its own — it runs each job's DUT image).
138
+ //- devices; SW: nothing of its own — a job's command brings what it runs).
139
139
  mixin capabilitiesList(caps)
140
140
  if !caps
141
141
  span.text-body-secondary —
142
142
  else if caps.sw
143
- span.text-body-secondary Runs each job's own DUT image (--docker-image), 2 CPUs / 2 GB
143
+ span.text-body-secondary Runs each job's command — nothing of its own to drive
144
144
  else if caps.hw
145
145
  - const kinds = [['stlinks', 'ST-Link'], ['uarts', 'UART'], ['usbs', 'USB']]
146
146
  - const empty = kinds.every(([k]) => !(caps.hw[k] || []).length)
@@ -332,7 +332,7 @@ mixin resourceCard(r, groupsById, returnTo)
332
332
  data-bs-target=`#rc-${r.id}-usb`
333
333
  aria-controls=`rc-${r.id}-usb`
334
334
  aria-selected="false"
335
- ) Connected USB devices
335
+ ) USB devices
336
336
 
337
337
  .tab-content
338
338
  .tab-pane.fade.show.active(id=`rc-${r.id}-details` role="tabpanel" aria-labelledby=`rc-${r.id}-details-tab` tabindex="0" data-live=`rc-details-${r.id}`)
@@ -346,7 +346,7 @@ mixin resourceCard(r, groupsById, returnTo)
346
346
  .tab-pane.fade.thub-usb-pane(id=`rc-${r.id}-usb` role="tabpanel" aria-labelledby=`rc-${r.id}-usb-tab` tabindex="0")
347
347
  +usbScanPane(r)
348
348
 
349
- //- "Connected USB devices" (resource card tab): the Client's `lsusb`, run
349
+ //- "USB devices" (resource card tab): the Client's `lsusb`, run
350
350
  //- only on Refresh (admins; public/js/usb-scan.js fetches and fills it in).
351
351
  mixin usbScanPane(r)
352
352
  - const offline = r.status === 'OUT_OF_SERVICE' || !r.last_heartbeat_at
@@ -67,6 +67,7 @@ block scripts
67
67
  script(src="/js/resource-card.js")
68
68
  script(src="/js/remove-resource.js")
69
69
  script(src="/js/client-config.js")
70
+ script(src="/js/tag-picker.js")
70
71
  script(src="/js/usb-scan.js")
71
72
  script(src="/js/usb-parse.js")
72
73
  script(src="/js/usb-import.js")