name: "[Demo] Deploy" on: workflow_dispatch: inputs: reseed: description: "Reseed demo data after deploy" type: boolean required: false default: true env: REGISTRY: ghcr.io IMAGE_NAME: torqvoice/torqvoice-demo jobs: build: runs-on: ubuntu-latest permissions: contents: read packages: write outputs: image: ${{ steps.meta.outputs.image }} steps: - name: Checkout demo branch uses: actions/checkout@v4 with: ref: demo - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Log in to GitHub Container Registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Get short commit SHA id: sha run: echo "short=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" - name: Set image reference id: meta run: echo "image=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" >> "$GITHUB_OUTPUT" - name: Build and push demo image uses: docker/build-push-action@v6 with: context: . push: true tags: | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:demo-${{ steps.sha.outputs.short }} build-args: | APP_VERSION=demo-${{ steps.sha.outputs.short }} cache-from: type=gha,scope=demo cache-to: type=gha,scope=demo,mode=min deploy: needs: build runs-on: [self-hosted, Linux, X64, hetzner] steps: - name: Checkout demo branch uses: actions/checkout@v4 with: ref: demo - name: Log in to GitHub Container Registry run: echo "${{ secrets.GHCR_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin - name: Create deployment files env: VIRTUAL_HOST: "demo.torqvoice.com" DATABASE_URL: ${{ secrets.DEMO_DATABASE_URL }} BETTER_AUTH_SECRET: ${{ secrets.DEMO_BETTER_AUTH_SECRET }} # Optional. Next encrypts what a server action closes over with a key # made fresh by every build, so a tab left open across a deploy can # no longer be read by the new container. A fixed key keeps those tabs # working. 32 random bytes, base64: `openssl rand -base64 32`. Left # unset, the build's own key is used, as before. NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: ${{ secrets.DEMO_NEXT_SERVER_ACTIONS_ENCRYPTION_KEY }} APP_URL: "https://demo.torqvoice.com" DEMO_USER_EMAIL: "demo@torqvoice.com" DEMO_USER_PASSWORD: "demo" DATA_PATH: ${{ secrets.DATA_PATH }} POSTHOG_HOST: ${{ secrets.NEXT_PUBLIC_POSTHOG_HOST }} POSTHOG_KEY: ${{ secrets.POSTHOG_DEMO_KEY }} DEMO_IMAGE: ${{ needs.build.outputs.image }} run: | mkdir -p $HOME/torqvoice-deploy/demo cd $HOME/torqvoice-deploy/demo env | grep -E '^(VIRTUAL_HOST|DATA_PATH|DATABASE_URL|BETTER_AUTH_SECRET|APP_URL|DEMO_|POSTHOG_|NEXT_SERVER_ACTIONS_)' > .env cat > docker-compose.yml << 'COMPOSE' networks: proxy: external: true name: proxy services: torqvoice-app-demo: container_name: torqvoice-app-demo image: ${DEMO_IMAGE}:latest restart: unless-stopped volumes: - ${DATA_PATH}/torqvoice_demo_data:/app/data expose: - "3000" environment: VIRTUAL_HOST: ${VIRTUAL_HOST} VIRTUAL_PORT: "3000" LETSENCRYPT_HOST: ${VIRTUAL_HOST} DATABASE_URL: ${DATABASE_URL} BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET} NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: ${NEXT_SERVER_ACTIONS_ENCRYPTION_KEY} NEXT_PUBLIC_APP_URL: ${APP_URL} DEMO_MODE: "true" DEMO_USER_EMAIL: ${DEMO_USER_EMAIL} DEMO_USER_PASSWORD: ${DEMO_USER_PASSWORD} POSTHOG_HOST: ${POSTHOG_HOST} POSTHOG_KEY: ${POSTHOG_KEY} networks: - proxy COMPOSE - name: Ensure data directory permissions run: | DATA_DIR="${{ secrets.DATA_PATH }}/torqvoice_demo_data" mkdir -p "$DATA_DIR" docker run --rm -v "$DATA_DIR":/data alpine chown -R 1001:1001 /data - name: Check free disk space run: | MIN_GB=5 DOCKER_ROOT=$(docker info --format '{{.DockerRootDir}}' 2>/dev/null || echo /var/lib/docker) AVAIL_KB=$(df -Pk "$DOCKER_ROOT" | awk 'NR==2 {print $4}') AVAIL_GB=$(awk "BEGIN {printf \"%.1f\", $AVAIL_KB/1024/1024}") echo "Free space on $DOCKER_ROOT: ${AVAIL_GB} GB (minimum ${MIN_GB} GB)" if [ "$AVAIL_KB" -lt $((MIN_GB * 1024 * 1024)) ]; then echo "::error::Not enough disk space to pull image: ${AVAIL_GB} GB free, need ${MIN_GB} GB. Aborting deploy." exit 1 fi - name: Deploy demo run: | cd $HOME/torqvoice-deploy/demo docker compose up -d --pull always - name: Health check run: | echo "Waiting for demo container to be ready..." for i in $(seq 1 12); do STATUS=$(docker inspect --format='{{.State.Status}}' torqvoice-app-demo 2>/dev/null || echo "not found") if [ "$STATUS" = "running" ]; then echo "Container is running!" exit 0 fi echo "Status: $STATUS, retrying in 5s..." sleep 5 done echo "::error::Container failed to start" docker logs torqvoice-app-demo --tail 50 exit 1 - name: Seed demo data if: ${{ github.event_name == 'push' || inputs.reseed }} run: | docker exec \ -e DEMO_USER_EMAIL="demo@torqvoice.com" \ -e DEMO_USER_PASSWORD="demo" \ -e DATA_ROOT=/app/data \ torqvoice-app-demo \ npx tsx prisma/seed_dummy_data.ts - name: Prune old images and build cache run: docker image prune -f && docker builder prune -f