diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b7794a0..b4dc575 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,206 +1,210 @@ -name: Deploy - -on: - push: - branches: - - main - -concurrency: - group: deploy-${{ github.repository }} - cancel-in-progress: true - -jobs: - deploy: - runs-on: ubuntu-latest - timeout-minutes: 120 - - steps: - - name: Deploy to Server - uses: appleboy/ssh-action@v1.0.3 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - REPO: ${{ github.repository }} - DEPLOY_ENV: ${{ secrets.DEPLOY_ENV }} - SUBMODULE_TOKEN: ${{ secrets.SUBMODULE_TOKEN }} - with: - host: ${{ secrets.DEPLOY_HOST }} - username: ${{ secrets.DEPLOY_USER }} - key: ${{ secrets.SSH_PRIVATE_KEY }} - envs: GITHUB_TOKEN,REPO,DEPLOY_ENV,SUBMODULE_TOKEN - timeout: 120m - command_timeout: 120m - script: | - set -euo pipefail - BASE_DIR="${{ secrets.DEPLOY_PATH || '/var/www/github-actions/repos' }}" - REPO_NAME=$(basename "$REPO") - REPO_DIR="$BASE_DIR/$REPO_NAME" - REPO_URL="https://x-access-token:${GITHUB_TOKEN}@github.com/${REPO}.git" - if [ ! -d "$REPO_DIR/.git" ]; then - mkdir -p "$BASE_DIR" - # Clone without recursing first; nested submodule auth is configured below. - for i in 1 2 3; do git clone "$REPO_URL" "$REPO_DIR" && break || sleep 5; done - fi - cd "$REPO_DIR" - git remote set-url origin "$REPO_URL" - SUBMODULE_AUTH="${SUBMODULE_TOKEN:-$GITHUB_TOKEN}" - set_submodule_url() { - local name="$1" - local repo="$2" - local url="https://x-access-token:${SUBMODULE_AUTH}@github.com/${repo}.git" - git config "submodule.${name}.url" "$url" - [ -d ".git/modules/${name}" ] && git config -f ".git/modules/${name}/config" remote.origin.url "$url" - } - for i in 1 2 3; do git fetch origin && break || { [ $i -eq 3 ] && echo "git fetch failed after 3 retries" && exit 1; sleep 5; }; done - git reset --hard origin/main - git submodule sync --recursive - # Configure submodule URLs after sync; otherwise cached SSH URLs or .gitmodules defaults can win. - set_submodule_url backend TashanGKD/Resonnet - set_submodule_url topiclab-cli TashanGKD/TopicLab-CLI - set_submodule_url ClawArcade TashanGKD/ClawArcade - set_submodule_url worldweave TashanGKD/worldweave - git submodule update --init backend - git config -f .git/modules/backend/config submodule.libs/assignable_skills/_submodules/ai-research.url "https://x-access-token:${SUBMODULE_AUTH}@github.com/Orchestra-Research/AI-Research-SKILLs.git" - git config -f .git/modules/backend/config submodule.libs/assignable_skills/_submodules/anthropics.url "https://x-access-token:${SUBMODULE_AUTH}@github.com/anthropics/skills.git" - git config -f .git/modules/backend/config submodule.libs/assignable_skills/_submodules/claude-scientific.url "https://x-access-token:${SUBMODULE_AUTH}@github.com/K-Dense-AI/claude-scientific-skills.git" - git config -f .git/modules/backend/config --get submodule.libs/assignable_skills/_submodules/claude-scientific.url | sed -E 's#x-access-token:[^@]+@#x-access-token:***@#' - git submodule update --init --recursive - - if [ -z "${DEPLOY_ENV:-}" ]; then - echo "::error::DEPLOY_ENV secret is not set. Add it in repo Settings → Secrets." - exit 1 - fi - echo "$DEPLOY_ENV" > .env - - run_with_heartbeat() { - local label="$1" - shift - "$@" & - local pid="$!" - while kill -0 "$pid" 2>/dev/null; do - echo "[deploy] ${label} still running at $(date -u +%Y-%m-%dT%H:%M:%SZ)" - sleep 30 - done - wait "$pid" - } - - run_with_heartbeat "docker compose build" docker compose build - docker compose down - docker compose up -d +name: Deploy + +on: + push: + branches: + - main + +concurrency: + group: deploy-${{ github.repository }} + cancel-in-progress: true + +jobs: + deploy: + runs-on: ubuntu-latest + timeout-minutes: 120 + + steps: + - name: Deploy to Server + uses: appleboy/ssh-action@v1.0.3 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + DEPLOY_ENV: ${{ secrets.DEPLOY_ENV }} + SUBMODULE_TOKEN: ${{ secrets.SUBMODULE_TOKEN }} + with: + host: ${{ secrets.DEPLOY_HOST }} + username: ${{ secrets.DEPLOY_USER }} + key: ${{ secrets.SSH_PRIVATE_KEY }} + envs: GITHUB_TOKEN,REPO,DEPLOY_ENV,SUBMODULE_TOKEN + timeout: 120m + command_timeout: 120m + script: | + set -euo pipefail + BASE_DIR="${{ secrets.DEPLOY_PATH || '/var/www/github-actions/repos' }}" + REPO_NAME=$(basename "$REPO") + REPO_DIR="$BASE_DIR/$REPO_NAME" + REPO_URL="https://x-access-token:${GITHUB_TOKEN}@github.com/${REPO}.git" + if [ ! -d "$REPO_DIR/.git" ]; then + mkdir -p "$BASE_DIR" + # Clone without recursing first; nested submodule auth is configured below. + for i in 1 2 3; do git clone "$REPO_URL" "$REPO_DIR" && break || sleep 5; done + fi + cd "$REPO_DIR" + git remote set-url origin "$REPO_URL" + SUBMODULE_AUTH="${SUBMODULE_TOKEN:-$GITHUB_TOKEN}" + set_submodule_url() { + local name="$1" + local repo="$2" + local url="https://x-access-token:${SUBMODULE_AUTH}@github.com/${repo}.git" + git config "submodule.${name}.url" "$url" + [ -d ".git/modules/${name}" ] && git config -f ".git/modules/${name}/config" remote.origin.url "$url" + } + for i in 1 2 3; do git fetch origin && break || { [ $i -eq 3 ] && echo "git fetch failed after 3 retries" && exit 1; sleep 5; }; done + git reset --hard origin/main + git submodule sync --recursive + # Configure submodule URLs after sync; otherwise cached SSH URLs or .gitmodules defaults can win. + set_submodule_url backend TashanGKD/Resonnet + set_submodule_url topiclab-cli TashanGKD/TopicLab-CLI + set_submodule_url ClawArcade TashanGKD/ClawArcade + set_submodule_url worldweave TashanGKD/worldweave + git submodule update --init backend + git config -f .git/modules/backend/config submodule.libs/assignable_skills/_submodules/ai-research.url "https://x-access-token:${SUBMODULE_AUTH}@github.com/Orchestra-Research/AI-Research-SKILLs.git" + git config -f .git/modules/backend/config submodule.libs/assignable_skills/_submodules/anthropics.url "https://x-access-token:${SUBMODULE_AUTH}@github.com/anthropics/skills.git" + git config -f .git/modules/backend/config submodule.libs/assignable_skills/_submodules/claude-scientific.url "https://x-access-token:${SUBMODULE_AUTH}@github.com/K-Dense-AI/claude-scientific-skills.git" + git config -f .git/modules/backend/config --get submodule.libs/assignable_skills/_submodules/claude-scientific.url | sed -E 's#x-access-token:[^@]+@#x-access-token:***@#' + git submodule update --init --recursive + + if [ -z "${DEPLOY_ENV:-}" ]; then + echo "::error::DEPLOY_ENV secret is not set. Add it in repo Settings → Secrets." + exit 1 + fi + echo "$DEPLOY_ENV" > .env + + run_with_heartbeat() { + local label="$1" + shift + "$@" & + local pid="$!" + while kill -0 "$pid" 2>/dev/null; do + echo "[deploy] ${label} still running at $(date -u +%Y-%m-%dT%H:%M:%SZ)" + sleep 30 + done + wait "$pid" + } + + run_with_heartbeat "docker compose build" docker compose build + docker compose down + docker compose up -d for i in $(seq 1 30); do if docker compose exec -T worldweave node -e "fetch('http://127.0.0.1:3020/api/v1/openclaw/skill.md').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"; then break fi - if [ "$i" = "30" ]; then - echo "::error::WorldWeave container did not become healthy on port 3020." - docker compose logs --tail=120 worldweave - exit 1 + if [ "$i" = "30" ]; then + echo "::error::WorldWeave container did not become healthy on port 3020." + docker compose logs --tail=120 worldweave + exit 1 fi sleep 2 done + if ! docker compose exec -T worldweave sh -lc "WORLD_HEALTH_BASE_URL=http://127.0.0.1:3020 WORLD_HEALTH_CHECK_SCENES=tech-ai,geo-politics-daily WORLD_HEALTH_FAIL_ON_REFRESH_DEGRADED=1 node scripts/health-world.mjs"; then + echo "::warning::WorldWeave scene freshness health check failed. Check the worldweave-refresh logs and source refresh status." + docker compose logs --tail=120 worldweave-refresh || true + fi docker image prune -f # Read frontend port from .env (default 3000); trim whitespace/CRLF to avoid nginx config issues - FRONTEND_PORT=$(grep -E '^FRONTEND_PORT=' .env 2>/dev/null | head -1 | cut -d= -f2- | tr -d ' \r' || echo 3000) - - # Write nginx snippet (location block only, included by world.tashan.chat server block). - # WorldWeave routes are handled inside the frontend container nginx. - # Requires VITE_BASE_PATH=/ in DEPLOY_ENV so frontend matches /api/. - mkdir -p /etc/nginx/snippets - printf '%s\n' \ - 'location / {' \ - " proxy_pass http://127.0.0.1:${FRONTEND_PORT};" \ - ' proxy_set_header Host $host;' \ - ' proxy_set_header X-Real-IP $remote_addr;' \ - ' proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;' \ - ' proxy_set_header X-Forwarded-Proto $scheme;' \ - '}' \ - 'location /api/ {' \ - " proxy_pass http://127.0.0.1:${FRONTEND_PORT}/api/;" \ - ' proxy_http_version 1.1;' \ - ' proxy_set_header Host $host;' \ - ' proxy_set_header X-Real-IP $remote_addr;' \ - ' proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;' \ - ' proxy_set_header X-Forwarded-Proto $scheme;' \ - ' proxy_set_header Connection "";' \ - ' proxy_buffering off;' \ - ' proxy_cache off;' \ - ' chunked_transfer_encoding on;' \ - ' proxy_read_timeout 600s;' \ - ' proxy_send_timeout 600s;' \ - '}' \ - | sudo tee /etc/nginx/snippets/world-tashan-chat.conf > /dev/null - sudo nginx -t && sudo nginx -s reload - - chmod +x scripts/deploy-clawarcade-reviewer.sh - scripts/deploy-clawarcade-reviewer.sh - - echo "--------------------------------------------" - echo "Frontend: http://${{ secrets.DEPLOY_HOST }}" - echo "Commit: ${{ github.sha }}" - echo "--------------------------------------------" - - cli-smoke: - name: Post-deploy CLI smoke (${{ matrix.cli_source }}) - needs: [deploy] - runs-on: ubuntu-latest - timeout-minutes: 30 - strategy: - fail-fast: false - matrix: - cli_source: [package, submodule] - env: - TOPICLAB_BASE_URL: https://world.tashan.chat - TOPICLAB_BIND_KEY: ${{ secrets.TOPICLAB_BIND_KEY }} - TOPICLAB_LIVE_SMOKE_ALLOW_WRITES: ${{ vars.TOPICLAB_LIVE_SMOKE_ALLOW_WRITES || '0' }} - - steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: "20" - cache: "npm" - cache-dependency-path: topiclab-cli/package-lock.json - - - name: Check write smoke secrets - run: | - if [[ "${TOPICLAB_LIVE_SMOKE_ALLOW_WRITES:-0}" == "1" && -z "${TOPICLAB_BIND_KEY:-}" ]]; then - echo "::error::TOPICLAB_BIND_KEY secret is required when TOPICLAB_LIVE_SMOKE_ALLOW_WRITES=1." - exit 1 - fi - - - name: Wait for deployed services to settle - run: sleep 60 - - - name: Install published topiclab-cli - if: matrix.cli_source == 'package' - run: | - npm install -g topiclab-cli@latest - npm list -g topiclab-cli --depth=0 || true - - - name: Install submodule topiclab-cli - if: matrix.cli_source == 'submodule' - run: | - pushd topiclab-cli - npm ci - npm run build - popd - npm install -g ./topiclab-cli - node -p "require('./topiclab-cli/package.json').version" - git -C topiclab-cli rev-parse HEAD - - - name: Run live smoke - run: | - chmod +x scripts/openclaw-live-skill-smoke.sh - args=( - --bind-key "${TOPICLAB_BIND_KEY}" - --base-url "${TOPICLAB_BASE_URL}" - --clean-cli-home - ) - if [[ "${TOPICLAB_LIVE_SMOKE_ALLOW_WRITES:-0}" == "1" ]]; then - args+=(--allow-writes) - fi - ./scripts/openclaw-live-skill-smoke.sh "${args[@]}" + FRONTEND_PORT=$(grep -E '^FRONTEND_PORT=' .env 2>/dev/null | head -1 | cut -d= -f2- | tr -d ' \r' || echo 3000) + + # Write nginx snippet (location block only, included by world.tashan.chat server block). + # WorldWeave routes are handled inside the frontend container nginx. + # Requires VITE_BASE_PATH=/ in DEPLOY_ENV so frontend matches /api/. + mkdir -p /etc/nginx/snippets + printf '%s\n' \ + 'location / {' \ + " proxy_pass http://127.0.0.1:${FRONTEND_PORT};" \ + ' proxy_set_header Host $host;' \ + ' proxy_set_header X-Real-IP $remote_addr;' \ + ' proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;' \ + ' proxy_set_header X-Forwarded-Proto $scheme;' \ + '}' \ + 'location /api/ {' \ + " proxy_pass http://127.0.0.1:${FRONTEND_PORT}/api/;" \ + ' proxy_http_version 1.1;' \ + ' proxy_set_header Host $host;' \ + ' proxy_set_header X-Real-IP $remote_addr;' \ + ' proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;' \ + ' proxy_set_header X-Forwarded-Proto $scheme;' \ + ' proxy_set_header Connection "";' \ + ' proxy_buffering off;' \ + ' proxy_cache off;' \ + ' chunked_transfer_encoding on;' \ + ' proxy_read_timeout 600s;' \ + ' proxy_send_timeout 600s;' \ + '}' \ + | sudo tee /etc/nginx/snippets/world-tashan-chat.conf > /dev/null + sudo nginx -t && sudo nginx -s reload + + chmod +x scripts/deploy-clawarcade-reviewer.sh + scripts/deploy-clawarcade-reviewer.sh + + echo "--------------------------------------------" + echo "Frontend: http://${{ secrets.DEPLOY_HOST }}" + echo "Commit: ${{ github.sha }}" + echo "--------------------------------------------" + + cli-smoke: + name: Post-deploy CLI smoke (${{ matrix.cli_source }}) + needs: [deploy] + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + cli_source: [package, submodule] + env: + TOPICLAB_BASE_URL: https://world.tashan.chat + TOPICLAB_BIND_KEY: ${{ secrets.TOPICLAB_BIND_KEY }} + TOPICLAB_LIVE_SMOKE_ALLOW_WRITES: ${{ vars.TOPICLAB_LIVE_SMOKE_ALLOW_WRITES || '0' }} + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: topiclab-cli/package-lock.json + + - name: Check write smoke secrets + run: | + if [[ "${TOPICLAB_LIVE_SMOKE_ALLOW_WRITES:-0}" == "1" && -z "${TOPICLAB_BIND_KEY:-}" ]]; then + echo "::error::TOPICLAB_BIND_KEY secret is required when TOPICLAB_LIVE_SMOKE_ALLOW_WRITES=1." + exit 1 + fi + + - name: Wait for deployed services to settle + run: sleep 60 + + - name: Install published topiclab-cli + if: matrix.cli_source == 'package' + run: | + npm install -g topiclab-cli@latest + npm list -g topiclab-cli --depth=0 || true + + - name: Install submodule topiclab-cli + if: matrix.cli_source == 'submodule' + run: | + pushd topiclab-cli + npm ci + npm run build + popd + npm install -g ./topiclab-cli + node -p "require('./topiclab-cli/package.json').version" + git -C topiclab-cli rev-parse HEAD + + - name: Run live smoke + run: | + chmod +x scripts/openclaw-live-skill-smoke.sh + args=( + --bind-key "${TOPICLAB_BIND_KEY}" + --base-url "${TOPICLAB_BASE_URL}" + --clean-cli-home + ) + if [[ "${TOPICLAB_LIVE_SMOKE_ALLOW_WRITES:-0}" == "1" ]]; then + args+=(--allow-writes) + fi + ./scripts/openclaw-live-skill-smoke.sh "${args[@]}" diff --git a/frontend/src/pages/__tests__/WorldWeaveProxyConfig.test.ts b/frontend/src/pages/__tests__/WorldWeaveProxyConfig.test.ts index f938841..6f6a89d 100644 --- a/frontend/src/pages/__tests__/WorldWeaveProxyConfig.test.ts +++ b/frontend/src/pages/__tests__/WorldWeaveProxyConfig.test.ts @@ -1,7 +1,7 @@ -import { readFileSync } from 'node:fs' -import { join } from 'node:path' -import { describe, expect, it } from 'vitest' - +import { readFileSync } from 'node:fs' +import { join } from 'node:path' +import { describe, expect, it } from 'vitest' + const CONFIG_PATHS = [ 'nginx.conf.template', 'nginx.root.conf', @@ -9,33 +9,34 @@ const CONFIG_PATHS = [ ] as const const WORLDWEAVE_DOCKERFILE_PATH = join(process.cwd(), '..', 'docker', 'worldweave.Dockerfile') - -function readConfig(path: (typeof CONFIG_PATHS)[number]) { - return readFileSync(join(process.cwd(), path), 'utf-8') -} - +const DEPLOY_WORKFLOW_PATH = join(process.cwd(), '..', '.github', 'workflows', 'deploy.yml') + +function readConfig(path: (typeof CONFIG_PATHS)[number]) { + return readFileSync(join(process.cwd(), path), 'utf-8') +} + describe('WorldWeave nginx proxy config', () => { - it.each(CONFIG_PATHS)('proxies WorldWeave app routes in %s', (path) => { - const config = readConfig(path) - - expect(config).toContain('location = /worldweave') - expect(config).toContain('location /worldweave/') - expect(config).toContain('location /_next/') - expect(config).toContain('location /api/v1/world/') - expect(config).toContain('location /api/v1/openclaw/') - expect(config).toContain('location /api/v1/signals') - expect(config).toContain('location = /signals') - expect(config).toContain('location /signals/') - expect(config).toContain('location /source-knowledge') - expect(config).toContain('location /demo/') - expect(config).toContain('location = /daily') - expect(config).toContain('location /daily/') - expect(config).toContain('location = /livebench') - expect(config).toContain('location /livebench/') - expect(config).toContain('worldweave:3020') - expect(config).toContain('proxy_set_header Host $http_host;') - expect(config).not.toContain('host.docker.internal:5000') - expect(config).not.toContain('host.docker.internal:3020') + it.each(CONFIG_PATHS)('proxies WorldWeave app routes in %s', (path) => { + const config = readConfig(path) + + expect(config).toContain('location = /worldweave') + expect(config).toContain('location /worldweave/') + expect(config).toContain('location /_next/') + expect(config).toContain('location /api/v1/world/') + expect(config).toContain('location /api/v1/openclaw/') + expect(config).toContain('location /api/v1/signals') + expect(config).toContain('location = /signals') + expect(config).toContain('location /signals/') + expect(config).toContain('location /source-knowledge') + expect(config).toContain('location /demo/') + expect(config).toContain('location = /daily') + expect(config).toContain('location /daily/') + expect(config).toContain('location = /livebench') + expect(config).toContain('location /livebench/') + expect(config).toContain('worldweave:3020') + expect(config).toContain('proxy_set_header Host $http_host;') + expect(config).not.toContain('host.docker.internal:5000') + expect(config).not.toContain('host.docker.internal:3020') }) it('seeds reviewed WorldWeave ASEAN model artifacts into the cache volume', () => { @@ -45,4 +46,13 @@ describe('WorldWeave nginx proxy config', () => { expect(dockerfile).toContain('cp -an /app/.seed-cache/. /app/.cache/') expect(dockerfile).toContain('node scripts/world-start.mjs') }) + + it('runs WorldWeave scene freshness checks during deploy', () => { + const workflow = readFileSync(DEPLOY_WORKFLOW_PATH, 'utf-8') + + expect(workflow).toContain('WORLD_HEALTH_BASE_URL=http://127.0.0.1:3020') + expect(workflow).toContain('WORLD_HEALTH_CHECK_SCENES=tech-ai,geo-politics-daily') + expect(workflow).toContain('node scripts/health-world.mjs') + expect(workflow).toContain('worldweave-refresh logs') + }) }) diff --git a/worldweave b/worldweave index a0f5c97..58b6018 160000 --- a/worldweave +++ b/worldweave @@ -1 +1 @@ -Subproject commit a0f5c970452075a3d43ba6e70b71f72d1df55f4b +Subproject commit 58b6018ae15414db1679a4b9ceb27ae92c36d2b4