From 83059111412014ea613625f6019740bd761c2d70 Mon Sep 17 00:00:00 2001 From: Agent Harness Date: Tue, 20 Oct 2026 21:20:17 +0100 Subject: [PATCH 1/3] ci: smoke-test the demo compose stack Brings up deploy/docker-compose.demo.yml on a clean runner, checks the dashboard answers and the pre-enrolled worker is online, records startup time in the job summary, and always tears down. Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/compose-smoke.yml | 91 +++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 .github/workflows/compose-smoke.yml diff --git a/.github/workflows/compose-smoke.yml b/.github/workflows/compose-smoke.yml new file mode 100644 index 0000000..103852b --- /dev/null +++ b/.github/workflows/compose-smoke.yml @@ -0,0 +1,91 @@ +name: Compose smoke + +# Brings up the one-command demo stack (deploy/docker-compose.demo.yml) on a +# clean runner and checks it works end to end: the dashboard answers and the +# pre-enrolled worker is online. Guards the demo against rot. + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + demo-stack: + name: Demo compose stack starts and enrols + runs-on: ubuntu-latest + timeout-minutes: 20 + env: + COMPOSE: docker compose -f deploy/docker-compose.demo.yml + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + + - name: Start the stack + run: | + echo "STARTED_AT=$(date +%s)" >> "$GITHUB_ENV" + # Not `up --wait`: the stack has one-shot services (certs, seed) + # that exit by design, which --wait can treat as a failure. + $COMPOSE up -d + + - name: Wait for the dashboard + run: | + for i in $(seq 1 120); do + if curl -fsS -o /dev/null http://localhost:8080/api/auth/config; then + echo "Dashboard answered after ~$((i * 5))s of polling" + exit 0 + fi + sleep 5 + done + echo "Dashboard did not answer on http://localhost:8080 in 10 minutes" >&2 + exit 1 + + - name: Dashboard serves HTML + run: | + curl -fsS http://localhost:8080/ | grep -qi ' /dev/null + api -X POST http://localhost:8080/api/auth/login \ + -d '{"username":"admin","password":"rsagent-demo"}' > /dev/null + for i in $(seq 1 90); do + if api http://localhost:8080/api/workers \ + | jq -e '.workers | any(.hostName == "DEMO-SQLHOST01" and .online == true)' > /dev/null; then + echo "Worker DEMO-SQLHOST01 is online" + exit 0 + fi + sleep 5 + done + echo "Worker DEMO-SQLHOST01 never came online" >&2 + api http://localhost:8080/api/workers | jq . >&2 || true + exit 1 + + - name: Record startup duration + if: success() + run: | + secs=$(( $(date +%s) - STARTED_AT )) + { + echo "### Demo stack startup" + echo + echo "Dashboard reachable and worker enrolled in **${secs}s** from \`compose up\`." + } >> "$GITHUB_STEP_SUMMARY" + + - name: Logs and container state + if: failure() + run: | + $COMPOSE ps -a + $COMPOSE logs --no-color --tail=200 + + - name: Tear down + if: always() + run: $COMPOSE down -v From 7845758b264ed9f84f3010847163340a09eb87a6 Mon Sep 17 00:00:00 2001 From: Agent Harness Date: Wed, 21 Oct 2026 12:19:41 +0100 Subject: [PATCH 2/3] fix(demo): mint enrolment token with a valid capability and stop retrying 4xx The server accepts observe, job.toggle, job.run, schedule.write, job.write and operator.write; readOnly is a worker.yaml ceiling tier, not a capability. mint-token now exits 78 on any 4xx and worker-entry.sh stops instead of re-running login until it is rate limited. Restarts are capped at 5. Co-Authored-By: Claude Sonnet 5.5 --- deploy/demo/mint-token.mjs | 37 ++++++++++++++++++++++++---------- deploy/demo/worker-entry.sh | 11 +++++++++- deploy/docker-compose.demo.yml | 2 +- 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/deploy/demo/mint-token.mjs b/deploy/demo/mint-token.mjs index 5050f27..9a74c9f 100644 --- a/deploy/demo/mint-token.mjs +++ b/deploy/demo/mint-token.mjs @@ -36,18 +36,33 @@ async function call(path, init = {}) { }); remember(response); if (!response.ok) { - throw new Error(`${init.method ?? 'GET'} ${path} -> ${response.status} ${await response.text()}`); + const error = new Error(`${init.method ?? 'GET'} ${path} -> ${response.status} ${await response.text()}`); + error.status = response.status; + throw error; } return response.json(); } -await call('/api/auth/config'); -await call('/api/auth/login', { - method: 'POST', - body: JSON.stringify({ username, password: adminPassword }), -}); -const minted = await call('/api/enrolment-tokens', { - method: 'POST', - body: JSON.stringify({ hostName, credentialMode: 'token', capabilities: ['readOnly'] }), -}); -process.stdout.write(minted.token); +async function mint() { + await call('/api/auth/config'); + await call('/api/auth/login', { + method: 'POST', + body: JSON.stringify({ username, password: adminPassword }), + }); + const minted = await call('/api/enrolment-tokens', { + method: 'POST', + body: JSON.stringify({ hostName, credentialMode: 'token', capabilities: ['observe'] }), + }); + return minted.token; +} + +// Exit 78 (EX_CONFIG) means the server rejected the request (4xx: bad +// credentials, bad capability, rate limited). Retrying cannot fix that, so +// worker-entry.sh stops instead of hammering /api/auth/login. Anything else +// (server still starting, network) exits 1 and is retried by compose. +try { + process.stdout.write(await mint()); +} catch (error) { + console.error(`[demo] ${error.message}`); + process.exit(error.status >= 400 && error.status < 500 ? 78 : 1); +} diff --git a/deploy/demo/worker-entry.sh b/deploy/demo/worker-entry.sh index c6296f2..d291e15 100644 --- a/deploy/demo/worker-entry.sh +++ b/deploy/demo/worker-entry.sh @@ -12,7 +12,16 @@ PKG=/app/worker-packages/rsagent-worker-linux.tar.gz tar -xzf "$PKG" -C "$RUN" ./rsagent-worker.mjs echo "[demo] minting an enrolment token as the bootstrap admin..." -TOKEN="$(node /demo/mint-token.mjs)" +rc=0 +TOKEN="$(node /demo/mint-token.mjs)" || rc=$? +if [ "$rc" -eq 78 ]; then + # The server answered with a 4xx. Retrying cannot help and would only burn + # the login rate limit, so exit 0 (compose's on-failure policy won't restart). + echo "[demo] the control plane rejected the enrolment request (see above); not retrying." >&2 + exit 0 +elif [ "$rc" -ne 0 ]; then + exit "$rc" +fi echo "[demo] enrolling ${RSAGENT_WORKER_HOST}..." node "$RUN/rsagent-worker.mjs" enrol --token "$TOKEN" /demo/worker.demo.yaml diff --git a/deploy/docker-compose.demo.yml b/deploy/docker-compose.demo.yml index 957699d..9f12479 100644 --- a/deploy/docker-compose.demo.yml +++ b/deploy/docker-compose.demo.yml @@ -195,7 +195,7 @@ services: cap_drop: - ALL read_only: true - restart: on-failure + restart: on-failure:5 mem_limit: 256m # --- Drift scenario -------------------------------------------------------- From 496155057e3f9fdc08e845d75db0c61f2ec07b1b Mon Sep 17 00:00:00 2001 From: Agent Harness Date: Wed, 21 Oct 2026 12:49:00 +0100 Subject: [PATCH 3/3] ci(compose-smoke): do not gate on 'up -d' exit code; log ps and worker output Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/compose-smoke.yml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/workflows/compose-smoke.yml b/.github/workflows/compose-smoke.yml index 103852b..3dae560 100644 --- a/.github/workflows/compose-smoke.yml +++ b/.github/workflows/compose-smoke.yml @@ -32,7 +32,17 @@ jobs: echo "STARTED_AT=$(date +%s)" >> "$GITHUB_ENV" # Not `up --wait`: the stack has one-shot services (certs, seed) # that exit by design, which --wait can treat as a failure. - $COMPOSE up -d + # `up -d` still waits on depends_on conditions (server healthy, seed + # completed) and exits non-zero if one is not met in time, e.g. while + # SQL Server warms up or a service restarts. The polling steps below + # are the real gate, so record the exit code and state, then go on. + rc=0 + $COMPOSE up -d || rc=$? + if [ "$rc" -ne 0 ]; then + echo "::warning::compose up exited $rc; continuing to the readiness checks" + $COMPOSE ps -a + $COMPOSE logs --no-color --tail=50 worker server seed certs || true + fi - name: Wait for the dashboard run: | @@ -84,6 +94,9 @@ jobs: if: failure() run: | $COMPOSE ps -a + echo "::group::worker logs" + $COMPOSE logs --no-color --tail=200 worker || true + echo "::endgroup::" $COMPOSE logs --no-color --tail=200 - name: Tear down