Barazo Docker Compose templates for self-hosting barazo.forum
at main 509 lines 20 kB view raw
1name: Deploy to Staging 2 3on: 4 repository_dispatch: 5 types: [deploy-staging] 6 workflow_dispatch: 7 inputs: 8 api_ref: 9 description: "barazo-api branch/tag (default: main)" 10 required: false 11 default: "main" 12 web_ref: 13 description: "barazo-web branch/tag (default: main)" 14 required: false 15 default: "main" 16 push: 17 branches: [main] 18 paths: 19 - "docker-compose*.yml" 20 - "Caddyfile" 21 - ".env.example" 22 23env: 24 REGISTRY: ghcr.io 25 DEPLOY_PATH: /opt/barazo 26 COMPOSE_FILES: "-f docker-compose.yml -f docker-compose.staging.yml" 27 28concurrency: 29 group: staging-deploy 30 cancel-in-progress: false 31 32jobs: 33 # ====================================================================== 34 # Job 1: Build and push Docker images to GHCR 35 # ====================================================================== 36 build: 37 name: Build Images 38 runs-on: ubuntu-latest 39 permissions: 40 contents: read 41 packages: write 42 timeout-minutes: 15 43 outputs: 44 api_ref: ${{ steps.refs.outputs.api_ref }} 45 web_ref: ${{ steps.refs.outputs.web_ref }} 46 trigger: ${{ steps.refs.outputs.trigger }} 47 48 steps: 49 - name: Determine checkout refs 50 id: refs 51 run: | 52 if [ "${{ github.event_name }}" = "repository_dispatch" ]; then 53 API_REF="${{ github.event.client_payload.api_ref || 'main' }}" 54 WEB_REF="${{ github.event.client_payload.web_ref || 'main' }}" 55 TRIGGER="${{ github.event.client_payload.trigger_repo || 'manual' }}" 56 elif [ "${{ github.event_name }}" = "push" ]; then 57 API_REF="main" 58 WEB_REF="main" 59 TRIGGER="barazo-deploy" 60 else 61 API_REF="${{ inputs.api_ref }}" 62 WEB_REF="${{ inputs.web_ref }}" 63 TRIGGER="workflow_dispatch" 64 fi 65 echo "api_ref=$API_REF" >> "$GITHUB_OUTPUT" 66 echo "web_ref=$WEB_REF" >> "$GITHUB_OUTPUT" 67 echo "trigger=$TRIGGER" >> "$GITHUB_OUTPUT" 68 echo "::notice::Building api@$API_REF web@$WEB_REF (trigger: $TRIGGER)" 69 70 # ------------------------------------------------------------------ 71 # Checkout all repos (public -- no PAT needed) 72 # ------------------------------------------------------------------ 73 - name: Checkout barazo-deploy 74 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 75 76 - name: Checkout barazo-workspace 77 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 78 with: 79 repository: singi-labs/barazo-workspace 80 path: _workspace 81 82 # Copy workspace root files (lockfile, catalogs) over deploy repo root. 83 # barazo-workspace is the single source of truth for dependency resolution. 84 - name: Sync workspace root files 85 run: | 86 cp _workspace/package.json . 87 cp _workspace/pnpm-lock.yaml . 88 cp _workspace/pnpm-workspace.yaml . 89 cp _workspace/.npmrc . 90 rm -rf _workspace 91 92 - name: Checkout barazo-lexicons 93 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 94 with: 95 repository: singi-labs/barazo-lexicons 96 path: barazo-lexicons 97 98 - name: Checkout barazo-plugins 99 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 100 with: 101 repository: singi-labs/barazo-plugins 102 path: barazo-plugins 103 104 - name: Checkout barazo-api 105 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 106 with: 107 repository: singi-labs/barazo-api 108 ref: ${{ steps.refs.outputs.api_ref }} 109 path: barazo-api 110 111 - name: Checkout barazo-web 112 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 113 with: 114 repository: singi-labs/barazo-web 115 ref: ${{ steps.refs.outputs.web_ref }} 116 path: barazo-web 117 118 # ------------------------------------------------------------------ 119 # Docker setup 120 # ------------------------------------------------------------------ 121 - name: Set up Docker Buildx 122 uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 123 124 - name: Login to Container Registry 125 uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 126 with: 127 registry: ${{ env.REGISTRY }} 128 username: ${{ github.actor }} 129 password: ${{ secrets.GITHUB_TOKEN }} 130 131 # ------------------------------------------------------------------ 132 # Build and push images (amd64 only for staging) 133 # ------------------------------------------------------------------ 134 - name: Build and push barazo-api 135 uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 136 with: 137 context: . 138 file: barazo-api/Dockerfile 139 push: true 140 tags: | 141 ${{ env.REGISTRY }}/singi-labs/barazo-api:edge 142 ${{ env.REGISTRY }}/singi-labs/barazo-api:staging-${{ github.run_number }} 143 cache-from: type=gha,scope=api 144 cache-to: type=gha,mode=max,scope=api 145 platforms: linux/amd64 146 147 - name: Build and push barazo-web 148 uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 149 with: 150 context: . 151 file: barazo-web/Dockerfile 152 push: true 153 tags: | 154 ${{ env.REGISTRY }}/singi-labs/barazo-web:edge 155 ${{ env.REGISTRY }}/singi-labs/barazo-web:staging-${{ github.run_number }} 156 cache-from: type=gha,scope=web 157 cache-to: type=gha,mode=max,scope=web 158 platforms: linux/amd64 159 160 # ====================================================================== 161 # Job 2: Smoke test -- verify images start and respond to health checks 162 # ====================================================================== 163 smoke-test: 164 name: Smoke Test 165 needs: build 166 runs-on: ubuntu-latest 167 permissions: 168 contents: read 169 packages: read 170 timeout-minutes: 5 171 172 env: 173 POSTGRES_USER: barazo 174 POSTGRES_PASSWORD: ci_smoke_test 175 POSTGRES_DB: barazo 176 VALKEY_PASSWORD: ci_smoke_test 177 DATABASE_URL: postgresql://barazo:ci_smoke_test@postgres:5432/barazo 178 TAP_ADMIN_PASSWORD: ci_smoke_test 179 SESSION_SECRET: ci_session_secret_not_real_extend_to_32 180 RELAY_URL: wss://bsky.network 181 COMMUNITY_DID: did:plc:ci-smoke-test 182 COMMUNITY_NAME: CI Smoke Test 183 COMMUNITY_DOMAIN: ":80" 184 COMMUNITY_MODE: single 185 OAUTH_CLIENT_ID: https://ci-smoke.barazo.forum/client-metadata.json 186 OAUTH_REDIRECT_URI: http://127.0.0.1/api/auth/callback 187 NEXT_PUBLIC_SITE_URL: http://127.0.0.1 188 BARAZO_API_VERSION: edge 189 BARAZO_WEB_VERSION: edge 190 191 steps: 192 - name: Checkout barazo-deploy 193 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 194 195 - name: Login to Container Registry 196 uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 197 with: 198 registry: ${{ env.REGISTRY }} 199 username: ${{ github.actor }} 200 password: ${{ secrets.GITHUB_TOKEN }} 201 202 - name: Create smoke test fixtures 203 run: | 204 # Empty plugin manifest (no plugins needed for smoke test) 205 echo '[]' > plugins.json 206 # No-op plugin install script 207 printf '#!/bin/sh\nexit 0\n' > scripts/install-plugins.sh 208 chmod +x scripts/install-plugins.sh 209 210 - name: Start full stack 211 run: docker compose -f docker-compose.yml up -d 212 213 - name: Wait for services to be healthy 214 run: | 215 echo "Waiting for services to become healthy..." 216 TIMEOUT=120 217 ELAPSED=0 218 INTERVAL=5 219 220 while [ $ELAPSED -lt $TIMEOUT ]; do 221 HEALTHY=$(docker compose -f docker-compose.yml ps --format json 2>/dev/null \ 222 | grep -c '"Health":"healthy"' || echo "0") 223 224 # postgres, valkey, barazo-api, barazo-web, caddy = 5 services with healthchecks 225 if [ "$HEALTHY" -ge 5 ]; then 226 echo "All services healthy after ${ELAPSED}s" 227 break 228 fi 229 230 echo " ${HEALTHY}/5 healthy (${ELAPSED}s elapsed)..." 231 sleep $INTERVAL 232 ELAPSED=$((ELAPSED + INTERVAL)) 233 done 234 235 if [ $ELAPSED -ge $TIMEOUT ]; then 236 echo "::error::Timed out waiting for services to be healthy" 237 docker compose -f docker-compose.yml ps 238 docker compose -f docker-compose.yml logs --tail=50 239 exit 1 240 fi 241 242 - name: Run smoke tests 243 run: ./scripts/smoke-test.sh 244 245 - name: Dump logs on failure 246 if: failure() 247 run: | 248 echo "=== Service Status ===" 249 docker compose -f docker-compose.yml ps 250 echo "" 251 echo "=== Service Logs ===" 252 docker compose -f docker-compose.yml logs --tail=100 253 254 - name: Tear down 255 if: always() 256 run: docker compose -f docker-compose.yml down -v 257 258 # ====================================================================== 259 # Job 3: Deploy to staging VPS (only if smoke test passes) 260 # ====================================================================== 261 deploy: 262 name: Deploy to Staging 263 needs: [build, smoke-test] 264 runs-on: ubuntu-latest 265 permissions: 266 contents: read 267 timeout-minutes: 10 268 269 steps: 270 - name: Sync configuration files to VPS 271 uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1 272 with: 273 host: staging.barazo.forum 274 username: deploy 275 key: ${{ secrets.STAGING_SSH_KEY }} 276 script: | 277 set -e 278 cd ${{ env.DEPLOY_PATH }} 279 echo "Syncing configuration files from repo (commit ${{ github.sha }})..." 280 BASE="https://raw.githubusercontent.com/singi-labs/barazo-deploy/${{ github.sha }}" 281 for f in docker-compose.yml docker-compose.staging.yml Caddyfile .env.example; do 282 wget -qO "$f.tmp" "$BASE/$f" 283 mv "$f.tmp" "$f" 284 echo " Updated $f" 285 done 286 echo "Configuration files synced" 287 288 - name: Save current image digests (for rollback) 289 uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1 290 id: save-digests 291 with: 292 host: staging.barazo.forum 293 username: deploy 294 key: ${{ secrets.STAGING_SSH_KEY }} 295 script: | 296 cd ${{ env.DEPLOY_PATH }} 297 echo "Saving current digests for rollback..." 298 docker inspect --format='{{index .RepoDigests 0}}' $(docker compose ${{ env.COMPOSE_FILES }} ps -q barazo-api 2>/dev/null) 2>/dev/null > /tmp/rollback-api-digest || echo "no-previous-api" 299 docker inspect --format='{{index .RepoDigests 0}}' $(docker compose ${{ env.COMPOSE_FILES }} ps -q barazo-web 2>/dev/null) 2>/dev/null > /tmp/rollback-web-digest || echo "no-previous-web" 300 echo "API digest: $(cat /tmp/rollback-api-digest)" 301 echo "Web digest: $(cat /tmp/rollback-web-digest)" 302 303 - name: Check for new required env vars 304 uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1 305 with: 306 host: staging.barazo.forum 307 username: deploy 308 key: ${{ secrets.STAGING_SSH_KEY }} 309 script: | 310 cd ${{ env.DEPLOY_PATH }} 311 echo "Checking for new required env vars..." 312 EXAMPLE_VARS=$(grep -v '^#' .env.example | grep -v '^\s*$' | grep '=' | cut -d= -f1 | sort) 313 CURRENT_VARS=$(grep -v '^#' .env | grep -v '^\s*$' | grep '=' | cut -d= -f1 | sort) 314 MISSING=$(comm -23 <(echo "$EXAMPLE_VARS") <(echo "$CURRENT_VARS")) 315 if [ -n "$MISSING" ]; then 316 echo "::warning::New env vars in .env.example not in .env:" 317 echo "$MISSING" 318 echo "" 319 echo "Deploy continues -- these may be optional. Check .env.example for details." 320 else 321 echo "All .env.example vars present in .env" 322 fi 323 324 - name: Pull new images and deploy 325 id: deploy 326 uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1 327 with: 328 host: staging.barazo.forum 329 username: deploy 330 key: ${{ secrets.STAGING_SSH_KEY }} 331 script: | 332 set -e 333 cd ${{ env.DEPLOY_PATH }} 334 335 echo "Pulling latest images..." 336 docker compose ${{ env.COMPOSE_FILES }} pull barazo-api barazo-web 337 338 echo "Starting containers..." 339 docker compose ${{ env.COMPOSE_FILES }} up -d 340 341 echo "Pruning unused images..." 342 docker image prune -f 343 344 echo "Deploy complete at $(date -u '+%Y-%m-%dT%H:%M:%SZ')" 345 346 # ------------------------------------------------------------------ 347 # Post-deploy health checks 348 # ------------------------------------------------------------------ 349 - name: Wait for containers to start 350 run: sleep 15 351 352 - name: Check container logs for crash patterns 353 id: log-check 354 uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1 355 with: 356 host: staging.barazo.forum 357 username: deploy 358 key: ${{ secrets.STAGING_SSH_KEY }} 359 script: | 360 cd ${{ env.DEPLOY_PATH }} 361 echo "Checking logs for crash patterns (last 30s)..." 362 363 LOGS=$(docker compose ${{ env.COMPOSE_FILES }} logs --since 30s barazo-api barazo-web 2>&1) 364 echo "$LOGS" 365 366 CRASH_PATTERNS="FATAL|ECONNREFUSED|missing env|ERR_MODULE_NOT_FOUND|Cannot find module|segfault|OOMKilled" 367 if echo "$LOGS" | grep -iE "$CRASH_PATTERNS"; then 368 echo "" 369 echo "CRASH_DETECTED=true" 370 echo "::error::Crash pattern detected in container logs" 371 exit 1 372 fi 373 374 # Check for containers that exited 375 EXITED=$(docker compose ${{ env.COMPOSE_FILES }} ps --format json 2>/dev/null | grep -i '"exited"' || true) 376 if [ -n "$EXITED" ]; then 377 echo "::error::One or more containers exited unexpectedly" 378 echo "$EXITED" 379 exit 1 380 fi 381 382 echo "No crash patterns detected" 383 384 - name: Verify health endpoints 385 id: health-check 386 uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1 387 with: 388 host: staging.barazo.forum 389 username: deploy 390 key: ${{ secrets.STAGING_SSH_KEY }} 391 script: | 392 cd ${{ env.DEPLOY_PATH }} 393 echo "Checking health endpoints..." 394 395 # Wait up to 90s for API to become healthy (Docker healthcheck: start-period 30s + interval 30s) 396 # Ports are not exposed to host, so we check Docker's own healthcheck status 397 for i in $(seq 1 18); do 398 API_HEALTH=$(docker inspect --format='{{.State.Health.Status}}' $(docker compose ${{ env.COMPOSE_FILES }} ps -q barazo-api 2>/dev/null) 2>/dev/null || echo "missing") 399 if [ "$API_HEALTH" = "healthy" ]; then 400 echo "API health: OK (healthy)" 401 break 402 fi 403 echo "API health: $API_HEALTH (attempt $i/18)" 404 sleep 5 405 done 406 407 if [ "$API_HEALTH" != "healthy" ]; then 408 echo "::error::API health check failed after 90s (status: $API_HEALTH)" 409 # Show recent logs for debugging 410 docker compose ${{ env.COMPOSE_FILES }} logs --tail=20 barazo-api 2>&1 || true 411 exit 1 412 fi 413 414 # Check web health 415 for i in $(seq 1 12); do 416 WEB_HEALTH=$(docker inspect --format='{{.State.Health.Status}}' $(docker compose ${{ env.COMPOSE_FILES }} ps -q barazo-web 2>/dev/null) 2>/dev/null || echo "missing") 417 if [ "$WEB_HEALTH" = "healthy" ]; then 418 echo "Web health: OK (healthy)" 419 break 420 fi 421 echo "Web health: $WEB_HEALTH (attempt $i/12)" 422 sleep 5 423 done 424 425 if [ "$WEB_HEALTH" != "healthy" ]; then 426 echo "::error::Web health check failed after 60s (status: $WEB_HEALTH)" 427 docker compose ${{ env.COMPOSE_FILES }} logs --tail=20 barazo-web 2>&1 || true 428 exit 1 429 fi 430 431 echo "All health checks passed" 432 433 - name: Verify login endpoint 434 id: login-check 435 uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1 436 with: 437 host: staging.barazo.forum 438 username: deploy 439 key: ${{ secrets.STAGING_SSH_KEY }} 440 script: | 441 set -e 442 cd ${{ env.DEPLOY_PATH }} 443 echo "Verifying login endpoint returns valid redirect URL..." 444 445 # Retry up to 6 times (30s total) -- API may still be starting after deploy 446 # Use 127.0.0.1 (not localhost) -- Alpine may not resolve localhost to IPv4 447 # Use a real Bluesky handle -- the API resolves the DID via the AT Protocol 448 for i in $(seq 1 6); do 449 RESPONSE=$(docker compose ${{ env.COMPOSE_FILES }} exec -T barazo-api \ 450 wget -qO- "http://127.0.0.1:3000/api/auth/login?handle=bsky.app" 2>&1 || true) 451 452 if echo "$RESPONSE" | grep -q '"url"'; then 453 echo "Login endpoint OK: response contains redirect URL (attempt $i/6)" 454 exit 0 455 fi 456 echo "Login check attempt $i/6 failed, retrying in 5s..." 457 sleep 5 458 done 459 460 echo "::warning::Login endpoint did not return expected {url} field after 6 attempts" 461 echo "Response: $RESPONSE" 462 echo "This may indicate an OAuth configuration issue" 463 464 # ------------------------------------------------------------------ 465 # Rollback on failure 466 # ------------------------------------------------------------------ 467 - name: Rollback on failure 468 if: failure() && (steps.deploy.outcome == 'failure' || steps.log-check.outcome == 'failure' || steps.health-check.outcome == 'failure') 469 uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1 470 with: 471 host: staging.barazo.forum 472 username: deploy 473 key: ${{ secrets.STAGING_SSH_KEY }} 474 script: | 475 cd ${{ env.DEPLOY_PATH }} 476 echo "::warning::Rolling back to previous images..." 477 478 API_DIGEST=$(cat /tmp/rollback-api-digest 2>/dev/null || echo "") 479 WEB_DIGEST=$(cat /tmp/rollback-web-digest 2>/dev/null || echo "") 480 481 if [ -n "$API_DIGEST" ] && [ "$API_DIGEST" != "no-previous-api" ]; then 482 echo "Rolling back API to: $API_DIGEST" 483 docker pull "$API_DIGEST" 484 docker tag "$API_DIGEST" ghcr.io/singi-labs/barazo-api:edge 485 fi 486 487 if [ -n "$WEB_DIGEST" ] && [ "$WEB_DIGEST" != "no-previous-web" ]; then 488 echo "Rolling back Web to: $WEB_DIGEST" 489 docker pull "$WEB_DIGEST" 490 docker tag "$WEB_DIGEST" ghcr.io/singi-labs/barazo-web:edge 491 fi 492 493 docker compose ${{ env.COMPOSE_FILES }} up -d 494 echo "Rollback complete. Waiting for health..." 495 sleep 15 496 497 API_STATUS=$(curl -sf -o /dev/null -w "%{http_code}" http://127.0.0.1:3000/api/health 2>/dev/null || echo "000") 498 echo "Post-rollback API health: $API_STATUS" 499 500 - name: Deploy summary 501 if: success() 502 run: | 503 echo "## Staging Deploy Complete" >> "$GITHUB_STEP_SUMMARY" 504 echo "" >> "$GITHUB_STEP_SUMMARY" 505 echo "- **Trigger:** ${{ needs.build.outputs.trigger }}" >> "$GITHUB_STEP_SUMMARY" 506 echo "- **API ref:** ${{ needs.build.outputs.api_ref }}" >> "$GITHUB_STEP_SUMMARY" 507 echo "- **Web ref:** ${{ needs.build.outputs.web_ref }}" >> "$GITHUB_STEP_SUMMARY" 508 echo "- **Run:** #${{ github.run_number }}" >> "$GITHUB_STEP_SUMMARY" 509 echo "- **URL:** https://staging.barazo.forum" >> "$GITHUB_STEP_SUMMARY"