Skip to content

Update all non-major updates #1797

Update all non-major updates

Update all non-major updates #1797

Workflow file for this run

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