Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions .github/workflows/compose-smoke.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
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.
# `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: |
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 '<html'

- name: Wait for the worker to enrol and come online
run: |
jar="$RUNNER_TEMP/cookies.txt"
api() { curl -fsS -b "$jar" -c "$jar" -H 'content-type: application/json' \
-H "x-rsagent-csrf: $(awk '$6=="rsagent_csrf"{print $7}' "$jar")" "$@"; }
api http://localhost:8080/api/auth/config > /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
echo "::group::worker logs"
$COMPOSE logs --no-color --tail=200 worker || true
echo "::endgroup::"
$COMPOSE logs --no-color --tail=200

- name: Tear down
if: always()
run: $COMPOSE down -v
37 changes: 26 additions & 11 deletions deploy/demo/mint-token.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
11 changes: 10 additions & 1 deletion deploy/demo/worker-entry.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion deploy/docker-compose.demo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ services:
cap_drop:
- ALL
read_only: true
restart: on-failure
restart: on-failure:5
mem_limit: 256m

# --- Drift scenario --------------------------------------------------------
Expand Down
Loading