Update all non-major updates #1797
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build, Push, and Deploy Docker Image | |
| on: | |
| push: | |
| branches: | |
| - main | |
| pull_request: | |
| branches: | |
| - main | |
| workflow_dispatch: | |
| inputs: | |
| deploy_tag: | |
| description: 'Tag to deploy (defaults to latest commit SHA)' | |
| required: false | |
| type: string | |
| jobs: | |
| build: | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v5 | |
| with: | |
| ref: ${{ github.sha }} | |
| - name: Cache Bun global cache | |
| # This step caches Bun's dependencies on the CI runner's filesystem | |
| uses: actions/cache@v4 | |
| with: | |
| # The path to Bun's global cache directory on the runner | |
| path: ~/.bun/install/cache | |
| # The key ensures we get a cache hit only if the lockfile is unchanged | |
| key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }} | |
| # The fallback key allows us to use a slightly stale cache | |
| restore-keys: | | |
| ${{ runner.os }}-bun- | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Log in to GitHub Container Registry | |
| run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin | |
| - name: Set repository name in lowercase | |
| id: prep | |
| run: | | |
| echo "REPO_NAME=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV | |
| # Sanitize branch name for docker tag | |
| BRANCH_TAG=$(echo ${{ github.ref_name }} | sed -e 's/[^a-zA-Z0-9.]/-/g') | |
| echo "BRANCH_TAG=$BRANCH_TAG" >> $GITHUB_ENV | |
| - name: Extract metadata (tags, labels) for Docker | |
| id: meta | |
| uses: docker/metadata-action@v5 | |
| with: | |
| images: | | |
| ghcr.io/${{ env.REPO_NAME }} | |
| tags: | | |
| latest | |
| type=sha | |
| - name: Build and push feedfathom-server image | |
| uses: docker/build-push-action@v6 | |
| timeout-minutes: 30 | |
| with: | |
| context: . | |
| target: feedfathom-server | |
| platforms: linux/amd64,linux/arm64,linux/arm64/v8 | |
| push: true | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| tags: | | |
| ghcr.io/${{ env.REPO_NAME }}/feedfathom-server:latest | |
| ghcr.io/${{ env.REPO_NAME }}/feedfathom-server:${{ github.sha }} | |
| ghcr.io/${{ env.REPO_NAME }}/feedfathom-server:${{ env.BRANCH_TAG }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| provenance: false | |
| build-args: | | |
| BUILDKIT_PROGRESS=plain | |
| BUILDKIT_STEP_LOG_MAX_SIZE=10485760 | |
| BUILDKIT_STEP_LOG_MAX_SPEED=10485760 | |
| GITHUB_REPOSITORY=${{ github.repository }} | |
| - name: Build and push feedfathom-worker image | |
| uses: docker/build-push-action@v6 | |
| timeout-minutes: 30 | |
| with: | |
| context: . | |
| target: feedfathom-worker | |
| platforms: linux/amd64,linux/arm64,linux/arm64/v8 | |
| push: true | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| tags: | | |
| ghcr.io/${{ env.REPO_NAME }}/feedfathom-worker:latest | |
| ghcr.io/${{ env.REPO_NAME }}/feedfathom-worker:${{ github.sha }} | |
| ghcr.io/${{ env.REPO_NAME }}/feedfathom-worker:${{ env.BRANCH_TAG }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| provenance: false | |
| build-args: | | |
| BUILDKIT_PROGRESS=plain | |
| BUILDKIT_STEP_LOG_MAX_SIZE=10485760 | |
| BUILDKIT_STEP_LOG_MAX_SPEED=10485760 | |
| GITHUB_REPOSITORY=${{ github.repository }} | |
| - name: Log out from GitHub Container Registry | |
| run: docker logout ghcr.io | |
| verify: | |
| runs-on: ubuntu-24.04 | |
| needs: build | |
| if: github.event_name == 'pull_request' || github.ref == 'refs/heads/main' | |
| timeout-minutes: 10 | |
| env: | |
| WORKER_REPLICAS: 1 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v5 | |
| with: | |
| ref: ${{ github.sha }} | |
| - name: Start services | |
| run: | | |
| export FEEDFATHOM_TAG=${{ github.sha }} && \ | |
| docker compose pull && \ | |
| docker compose up -d | |
| - name: Wait for services to be healthy | |
| run: | | |
| echo "Waiting for services to be healthy..." | |
| MAX_ATTEMPTS=150 | |
| attempts=0 | |
| until ! docker compose ps | grep -v "(healthy)" | grep -q "Up"; do | |
| attempts=$((attempts + 1)) | |
| if [ $attempts -ge $MAX_ATTEMPTS ]; then | |
| echo "Timeout waiting for services to become healthy after 5 minutes" >&2 | |
| echo "Current status:" >&2 | |
| docker compose ps >&2 | |
| exit 1 | |
| fi | |
| echo "Waiting for all services to be healthy... (attempt $attempts/$MAX_ATTEMPTS)" | |
| sleep 2 | |
| done | |
| echo "All services are now healthy" | |
| - name: Verify web server is responding | |
| run: | | |
| echo "Verifying web server response..." | |
| MAX_ATTEMPTS=30 | |
| attempts=0 | |
| # First check if the server container is running | |
| echo "Checking server container status..." | |
| docker compose ps server | |
| until RESPONSE=$(curl -v -s -w "\n%{http_code}" http://localhost:3456 2>&1) && STATUS_CODE=$(echo "$RESPONSE" | tail -n1) && echo "$STATUS_CODE" | grep -q "^[23]"; do | |
| attempts=$((attempts + 1)) | |
| if [ $attempts -ge $MAX_ATTEMPTS ]; then | |
| echo "Timeout waiting for web server to respond with non-error status after 1 minute" >&2 | |
| echo "Current response:" >&2 | |
| echo "$RESPONSE" >&2 | |
| echo "Server container logs:" >&2 | |
| docker compose logs server >&2 | |
| echo "Server container status:" >&2 | |
| docker compose ps server >&2 | |
| exit 1 | |
| fi | |
| echo "Waiting for web server to respond with non-error status... (attempt $attempts/$MAX_ATTEMPTS)" | |
| echo "Current response:" >&2 | |
| echo "$RESPONSE" >&2 | |
| sleep 2 | |
| done | |
| echo "Web server response body:" | |
| echo "$RESPONSE" | sed '$d' | |
| echo "Web server is responding with status code: $STATUS_CODE" | |
| - name: Print logs on failure | |
| if: failure() | |
| run: | | |
| echo "Verification failed. Printing logs for all services:" | |
| docker compose logs migrator worker mail server | |
| echo "Current container status:" | |
| docker compose ps | |
| - name: Cleanup | |
| if: always() | |
| run: docker compose down | |
| deploy: | |
| runs-on: ubuntu-24.04 | |
| needs: [build, verify] | |
| if: | | |
| (github.ref == 'refs/heads/main') || | |
| (github.event_name == 'workflow_dispatch') | |
| env: | |
| DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }} | |
| ALLOWED_EMAILS: ${{ vars.ALLOWED_EMAILS }} | |
| ENABLE_REGISTRATION: ${{ vars.ENABLE_REGISTRATION }} | |
| WORKER_CONCURRENCY: ${{ vars.WORKER_CONCURRENCY }} | |
| LOCK_DURATION: ${{ vars.LOCK_DURATION }} | |
| CLEANUP_INTERVAL: ${{ vars.CLEANUP_INTERVAL }} | |
| GATHER_JOBS_INTERVAL: ${{ vars.GATHER_JOBS_INTERVAL }} | |
| APP_REPLICAS: ${{ vars.APP_REPLICAS }} | |
| WORKER_REPLICAS: ${{ vars.WORKER_REPLICAS }} | |
| WORKER_MEM_LIMIT: ${{ vars.WORKER_MEM_LIMIT }} | |
| FEED_FATHOM_ALT_DOMAIN: ${{ vars.FEED_FATHOM_ALT_DOMAIN }} | |
| MAIL_ENABLED: ${{ vars.MAIL_ENABLED }} | |
| MAIL_REPLICAS: ${{ vars.MAIL_REPLICAS }} | |
| MAILJET_API_KEY: ${{ secrets.MAILJET_API_KEY }} | |
| MAILJET_API_SECRET: ${{ secrets.MAILJET_API_SECRET }} | |
| TURNSTILE_SITE_KEY: ${{ secrets.TURNSTILE_SITE_KEY }} | |
| TURNSTILE_SECRET_KEY: ${{ secrets.TURNSTILE_SECRET_KEY }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v5 | |
| with: | |
| ref: ${{ github.sha }} | |
| sparse-checkout: | | |
| docker-compose.yml | |
| docker-compose-traefik.yml | |
| rollout.ts | |
| - name: Add SSH key | |
| uses: webfactory/ssh-agent@v0.10.0 | |
| with: | |
| ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} | |
| - name: Copy Docker Compose files and rollout script to server | |
| run: | | |
| scp -o StrictHostKeyChecking=no docker-compose.yml docker-compose-traefik.yml rollout.ts ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:${{ env.DEPLOY_PATH }}/ | |
| - name: Deploy to server (with auto-retry on SSH disconnect) | |
| timeout-minutes: 60 | |
| run: | | |
| set -euo pipefail | |
| SSH_DEST="${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}" | |
| SSH_OPTS="-o StrictHostKeyChecking=no -o BatchMode=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=3" | |
| # Remote command assembled as a single line to avoid YAML parsing issues | |
| REMOTE_CMD="cd ${{ env.DEPLOY_PATH }} && export PATH=\"/home/${{ secrets.SSH_USER }}/.bun/bin:\$PATH\" && export FEEDFATHOM_TAG=${{ github.event.inputs.deploy_tag || github.sha }} && export MAIL_REPLICAS='${{ env.MAIL_REPLICAS }}' && export FEED_FATHOM_DOMAIN='${{ secrets.FEED_FATHOM_DOMAIN }}' && export FEED_FATHOM_ALT_DOMAIN='${{ env.FEED_FATHOM_ALT_DOMAIN }}' && export ALLOWED_EMAILS='${{ env.ALLOWED_EMAILS }}' && export ENABLE_REGISTRATION='${{ env.ENABLE_REGISTRATION }}' && export WORKER_CONCURRENCY='${{ env.WORKER_CONCURRENCY }}' && export LOCK_DURATION='${{ env.LOCK_DURATION }}' && export CLEANUP_INTERVAL='${{ env.CLEANUP_INTERVAL }}' && export GATHER_JOBS_INTERVAL='${{ env.GATHER_JOBS_INTERVAL }}' && export WORKER_REPLICAS='${{ env.WORKER_REPLICAS }}' && export WORKER_MEM_LIMIT='${{ env.WORKER_MEM_LIMIT }}' && export MAIL_ENABLED='${{ env.MAIL_ENABLED }}' && export MAILJET_API_KEY='${{ env.MAILJET_API_KEY }}' && export MAILJET_API_SECRET='${{ env.MAILJET_API_SECRET }}' && export TURNSTILE_SITE_KEY='${{ env.TURNSTILE_SITE_KEY }}' && export TURNSTILE_SECRET_KEY='${{ env.TURNSTILE_SECRET_KEY }}' && docker compose -f docker-compose.yml -f docker-compose-traefik.yml pull migrator mail server worker && bun run rollout.ts -f docker-compose.yml -f docker-compose-traefik.yml migrator mail server worker" | |
| MAX_RETRIES=10 | |
| DELAY=30 | |
| for attempt in $(seq 1 $MAX_RETRIES); do | |
| echo "🚀 Deployment attempt $attempt/$MAX_RETRIES" | |
| # shellcheck disable=SC2029 | |
| if ssh $SSH_OPTS "$SSH_DEST" "${REMOTE_CMD}"; then | |
| echo "✅ Deployment succeeded on attempt $attempt" | |
| exit 0 | |
| fi | |
| rc=$? | |
| if [[ $rc -ne 255 ]]; then | |
| echo "❌ Deployment failed with exit-code $rc (not an SSH disconnect)" | |
| exit $rc | |
| fi | |
| echo "⚠️ SSH connection lost (exit-code 255). Waiting $DELAY s before retry…" | |
| sleep "$DELAY" | |
| done | |
| echo "❌ Deployment failed after $MAX_RETRIES retries due to repeated SSH disconnects." | |
| exit 1 |