Add Gitea restart workflow and link-style "Run again" for the public view
Test and deploy enrollment demo / test (push) Successful in 36s
Test and deploy enrollment demo / deploy (push) Successful in 1m13s

The server deployment cannot mount the Docker socket safely, so the web
view's button now opens the Gitea page for a workflow_dispatch workflow
that recreates the stack and replays the demo. Gitea handles who may run it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 21:54:24 +03:30
co-authored by Claude Sonnet 5.5
parent bf40b92c52
commit 37ca9d4da1
7 changed files with 49 additions and 8 deletions
+1
View File
@@ -28,6 +28,7 @@ jobs:
- name: Deploy on the server's Docker daemon (viewer published on port 8099) - name: Deploy on the server's Docker daemon (viewer published on port 8099)
env: env:
VIEWER_BIND: "0.0.0.0" VIEWER_BIND: "0.0.0.0"
RESTART_URL: https://git.my-rm.com/Robot-Market/mqtt-security-test/actions?workflow=restart.yml
COMPOSE_PROJECT_NAME: crypto-interop-spike COMPOSE_PROJECT_NAME: crypto-interop-spike
run: | run: |
set -eu set -eu
+24
View File
@@ -0,0 +1,24 @@
name: Restart demo
# Replays the whole demo on the server without rebuilding anything. Run it from Gitea (Actions, then
# "Run workflow"); only users with access to the repository can do that, which is why the public web
# view has no restart button of its own. The web view's "Run again (via Gitea)" button opens this page.
on:
workflow_dispatch:
concurrency:
group: mqtt-security-test
cancel-in-progress: false
jobs:
restart:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Recreate the stack (fresh broker, new run); images are reused, built only if missing
env:
VIEWER_BIND: "0.0.0.0"
RESTART_URL: https://git.my-rm.com/Robot-Market/mqtt-security-test/actions?workflow=restart.yml
COMPOSE_PROJECT_NAME: crypto-interop-spike
run: docker compose -f docker/compose.yaml up -d --force-recreate --remove-orphans
- name: Wait for the run and require every board check to pass
env:
COMPOSE_PROJECT_NAME: crypto-interop-spike
run: sh scripts/ci-verify.sh
+1 -1
View File
@@ -118,7 +118,7 @@ Visible on the web page in addition: the two **fingerprints** (backend and board
- **"Now" box:** a plain-language explanation of the latest step. - **"Now" box:** a plain-language explanation of the latest step.
- **Options:** follow live, only checks and steps, hide countdown lines. Click a long hex value to expand it. - **Options:** follow live, only checks and steps, hide countdown lines. Click a long hex value to expand it.
- A refresh replays the whole run, because the server keeps every line. - A refresh replays the whole run, because the server keeps every line.
- **Run again** button (shown only when the stack is started with `docker/compose.restart.yaml`, as `run-docker.sh` does; the server deployment does not use it): restarts the backend, both boards and the broker (clean Dynamic Security state), clears the logs and replays the whole run. It needs the Docker socket mounted into the `viewer` container (`/var/run/docker.sock`), which gives that container control of Docker on the host, so the page is bound to `127.0.0.1` and the restart endpoint requires a custom header that other web pages cannot send. Demo use only. - **Run again** button (shown only when the stack is started with `docker/compose.restart.yaml`, as `run-docker.sh` does; the server deployment does not use it; there the button opens the Gitea "Restart demo" workflow page instead): restarts the backend, both boards and the broker (clean Dynamic Security state), clears the logs and replays the whole run. It needs the Docker socket mounted into the `viewer` container (`/var/run/docker.sock`), which gives that container control of Docker on the host, so the page is bound to `127.0.0.1` and the restart endpoint requires a custom header that other web pages cannot send. Demo use only.
## 8. File map ## 8. File map
+5 -3
View File
@@ -20,9 +20,11 @@ cd docker && ./run-docker.sh # web view on http://localhost:8099, with the
Every push to `main` runs `.gitea/workflows/deploy.yml` on the Gitea runner: it validates the compose Every push to `main` runs `.gitea/workflows/deploy.yml` on the Gitea runner: it validates the compose
files, builds all images, redeploys the stack on the server's Docker daemon with the web view published on files, builds all images, redeploys the stack on the server's Docker daemon with the web view published on
**port 8099** (`http://<server-ip>:8099`) and fails unless both boards pass all checks. **port 8099** (`http://<server-ip>:8099`) and fails unless both boards pass all checks.
The public deployment has no "Run again" button, because that needs the Docker socket; to replay the run, The public web view cannot restart anything itself (that would need the Docker socket). Its **"Run again (via Gitea)"**
start the workflow manually (Actions, "Run workflow"). To run the server stack with the button anyway, button opens the Gitea Actions page for `.gitea/workflows/restart.yml`; run that workflow there ("Run workflow") to replay
add `-f docker/compose.restart.yaml` and understand it gives the viewer control of Docker on the host. the whole demo on the server without rebuilding. Only people with access to the repository can do that. To run a stack
locally with a self-contained button instead, use `docker/run-docker.sh` (it adds `docker/compose.restart.yaml`, which
mounts the Docker socket: local use only).
The compose file contains **demo secrets** (broker and service passwords, a signing seed) that protect nothing The compose file contains **demo secrets** (broker and service passwords, a signing seed) that protect nothing
real. Do not reuse them anywhere else. real. Do not reuse them anywhere else.
+2
View File
@@ -23,6 +23,8 @@ services:
- "${VIEWER_BIND:-127.0.0.1}:8099:8080" - "${VIEWER_BIND:-127.0.0.1}:8099:8080"
environment: environment:
COMPOSE_PROJECT: crypto-interop-spike COMPOSE_PROJECT: crypto-interop-spike
# When set (server deployment), the page offers "Run again" as a link to the Gitea workflow page.
RESTART_URL: ${RESTART_URL:-}
volumes: volumes:
- logs:/logs - logs:/logs
# The "Run again" button is NOT available here; it needs compose.restart.yaml (Docker socket). # The "Run again" button is NOT available here; it needs compose.restart.yaml (Docker socket).
+11 -2
View File
@@ -239,7 +239,10 @@ for (const [id, cls] of [['onlychecks', 'only-checks'], ['hidedetail', 'hide-det
// refresh shows the whole run again. // refresh shows the whole run again.
// Run again: ask the viewer server to restart the whole demo (backend, boards and a fresh broker). // Run again: ask the viewer server to restart the whole demo (backend, boards and a fresh broker).
// The custom header is required by the server so other web pages cannot trigger it. // The custom header is required by the server so other web pages cannot trigger it.
let cfg = { restart: false, restartUrl: '' };
document.getElementById('restart').addEventListener('click', async () => { document.getElementById('restart').addEventListener('click', async () => {
// Public deployment: no Docker access here, so send the user to Gitea, which requires a login.
if (!cfg.restart && cfg.restartUrl) { window.open(cfg.restartUrl, '_blank', 'noopener'); return; }
document.getElementById('restart').disabled = true; document.getElementById('restart').disabled = true;
try { try {
const r = await fetch('/api/restart', { method: 'POST', headers: { 'X-Demo-Action': 'restart' } }); const r = await fetch('/api/restart', { method: 'POST', headers: { 'X-Demo-Action': 'restart' } });
@@ -259,7 +262,13 @@ function resetView() {
} }
// The restart button only exists when the server was started with restart support enabled. // The restart button only exists when the server was started with restart support enabled.
fetch('/api/config').then(r => r.json()).then(c => { if (!c.restart) document.getElementById('restart').style.display = 'none'; }) fetch('/api/config').then(r => r.json()).then(c => {
cfg = c;
const b = document.getElementById('restart');
if (c.restart) return;
if (c.restartUrl) { b.innerHTML = '&#8635; Run again (via Gitea)'; b.title = 'Opens Gitea, where you run the "Restart demo" workflow (login required)'; }
else b.style.display = 'none';
})
.catch(() => { document.getElementById('restart').style.display = 'none'; }); .catch(() => { document.getElementById('restart').style.display = 'none'; });
buildCards(); buildCards();
@@ -271,7 +280,7 @@ es.onmessage = e => add(JSON.parse(e.data));
es.addEventListener('reset', resetView); es.addEventListener('reset', resetView);
es.addEventListener('status', e => { es.addEventListener('status', e => {
const s = JSON.parse(e.data); const s = JSON.parse(e.data);
document.getElementById('restart').disabled = s.busy; if (cfg.restart) document.getElementById('restart').disabled = s.busy;
document.getElementById('status').textContent = s.busy || s.text !== 'ready' ? s.text : liveText; document.getElementById('status').textContent = s.busy || s.text !== 'ready' ? s.text : liveText;
}); });
</script> </script>
+4 -1
View File
@@ -45,6 +45,9 @@ var (
logDir = "/logs" logDir = "/logs"
// The restart button needs the Docker socket; it is only enabled when the deployment says so. // The restart button needs the Docker socket; it is only enabled when the deployment says so.
restartEnabled = os.Getenv("RESTART_ENABLED") == "1" restartEnabled = os.Getenv("RESTART_ENABLED") == "1"
// Without the Docker socket the page can still offer "Run again" as a link to this URL (the
// Gitea page that runs the restart workflow); Gitea then does the authentication.
restartURL = os.Getenv("RESTART_URL")
re = regexp.MustCompile(`^(\d\d:\d\d:\d\d\.\d{3}) \[([a-z-]+)\] (.*)$`) re = regexp.MustCompile(`^(\d\d:\d\d:\d\d\.\d{3}) \[([a-z-]+)\] (.*)$`)
) )
@@ -294,7 +297,7 @@ func main() {
json.NewEncoder(w).Encode(lines) json.NewEncoder(w).Encode(lines)
}) })
http.HandleFunc("/api/config", func(w http.ResponseWriter, r *http.Request) { http.HandleFunc("/api/config", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]bool{"restart": restartEnabled}) json.NewEncoder(w).Encode(map[string]any{"restart": restartEnabled, "restartUrl": restartURL})
}) })
http.HandleFunc("/api/restart", func(w http.ResponseWriter, r *http.Request) { http.HandleFunc("/api/restart", func(w http.ResponseWriter, r *http.Request) {
if !restartEnabled { if !restartEnabled {