From fbaac6a9ba96fb61155248bedd59e2312ad972aa Mon Sep 17 00:00:00 2001 From: "Kai (Tam Nhu) Tran" <61256810+kaitranntt@users.noreply.github.com> Date: Sat, 16 May 2026 12:39:50 -0400 Subject: [PATCH] feat(docker): canonical compose.yaml + smoke-test (P2 of #1251) (#1258) * feat(docker): add canonical compose.yaml for zero-install flow (#1251) * ci(docker): smoke-test ccs.kaitran.ca/docker-compose.yaml on release + nightly (#1251) --- .github/workflows/smoke-test-compose-url.yml | 61 ++++++++++++++++++++ docker/compose.yaml | 39 +++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 .github/workflows/smoke-test-compose-url.yml create mode 100644 docker/compose.yaml diff --git a/.github/workflows/smoke-test-compose-url.yml b/.github/workflows/smoke-test-compose-url.yml new file mode 100644 index 00000000..b9b47577 --- /dev/null +++ b/.github/workflows/smoke-test-compose-url.yml @@ -0,0 +1,61 @@ +name: Smoke Test – ccs.kaitran.ca/docker-compose.yaml + +on: + release: + types: + - published + schedule: + # Nightly at 07:00 UTC + - cron: "0 7 * * *" + workflow_dispatch: + inputs: + do_up: + description: > + Also run `docker compose up -d`, wait for healthcheck, curl ports, then + `docker compose down -v`. Skipped by default (parse-only). + required: false + type: boolean + default: false + +jobs: + smoke-test: + name: curl + parse (+ optional up/down) + runs-on: [self-hosted, linux, x64, cliproxy] + + steps: + - name: Download compose from canonical URL + run: | + curl -fsSL https://ccs.kaitran.ca/docker-compose.yaml -o /tmp/ccs-compose.yaml + echo "--- downloaded compose ---" + cat /tmp/ccs-compose.yaml + + - name: Validate compose parses (docker compose config) + run: docker compose -f /tmp/ccs-compose.yaml config + + - name: Bring stack up, verify healthcheck, tear down + if: ${{ github.event_name == 'workflow_dispatch' && inputs.do_up == true }} + run: | + set -euo pipefail + + docker compose -f /tmp/ccs-compose.yaml up -d + + echo "Waiting for healthcheck to pass (max 90s)..." + for i in $(seq 1 18); do + STATUS=$(docker compose -f /tmp/ccs-compose.yaml ps --format json \ + | python3 -c "import sys,json; data=sys.stdin.read().strip(); rows=json.loads('['+','.join(data.splitlines())+']') if data else []; print(next((r.get('Health','') for r in rows if 'ccs' in r.get('Service','')), 'unknown'))" 2>/dev/null || echo "unknown") + echo "Health: $STATUS" + if [ "$STATUS" = "healthy" ]; then + break + fi + sleep 5 + done + + # Verify ports respond + curl -fsSL --retry 3 --retry-delay 2 http://localhost:3000 -o /dev/null || \ + { echo "[X] Dashboard port 3000 did not respond"; docker compose -f /tmp/ccs-compose.yaml down -v; exit 1; } + + curl -fsSL --retry 3 --retry-delay 2 http://localhost:8317 -o /dev/null || \ + { echo "[X] CLIProxy port 8317 did not respond"; docker compose -f /tmp/ccs-compose.yaml down -v; exit 1; } + + echo "[OK] Both ports healthy" + docker compose -f /tmp/ccs-compose.yaml down -v diff --git a/docker/compose.yaml b/docker/compose.yaml new file mode 100644 index 00000000..70cb5239 --- /dev/null +++ b/docker/compose.yaml @@ -0,0 +1,39 @@ +# Canonical CCS docker-compose. Served at https://ccs.kaitran.ca/docker-compose.yaml. +# Source of truth: kaitranntt/ccs:docker/compose.yaml +# +# Quick start: +# curl -fsSL https://ccs.kaitran.ca/docker-compose.yaml -o compose.yaml +# docker compose up -d +# +# Ports: +# 3000 — CCS Dashboard +# 8317 — CLIProxy API + +services: + ccs: + image: ghcr.io/kaitranntt/ccs:latest + restart: unless-stopped + ports: + - "3000:3000" + - "8317:8317" + volumes: + - ccs_home:/root/.ccs + - ccs_logs:/var/log/ccs + networks: + - ccs-net + healthcheck: + test: + - "CMD-SHELL" + - > + node -e "require('http').get('http://127.0.0.1:8317/',r=>process.exit(r.statusCode<500?0:1)).on('error',()=>process.exit(1))" + interval: 30s + timeout: 5s + retries: 3 + +volumes: + ccs_home: + ccs_logs: + +networks: + ccs-net: + name: ccs-net