deploy-demo.yml 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. name: "[Demo] Deploy"
  2. on:
  3. workflow_dispatch:
  4. inputs:
  5. reseed:
  6. description: "Reseed demo data after deploy"
  7. type: boolean
  8. required: false
  9. default: true
  10. env:
  11. REGISTRY: ghcr.io
  12. IMAGE_NAME: torqvoice/torqvoice-demo
  13. jobs:
  14. build:
  15. runs-on: ubuntu-latest
  16. permissions:
  17. contents: read
  18. packages: write
  19. outputs:
  20. image: ${{ steps.meta.outputs.image }}
  21. steps:
  22. - name: Checkout demo branch
  23. uses: actions/checkout@v4
  24. with:
  25. ref: demo
  26. - name: Set up Docker Buildx
  27. uses: docker/setup-buildx-action@v3
  28. - name: Log in to GitHub Container Registry
  29. uses: docker/login-action@v3
  30. with:
  31. registry: ${{ env.REGISTRY }}
  32. username: ${{ github.actor }}
  33. password: ${{ secrets.GITHUB_TOKEN }}
  34. - name: Get short commit SHA
  35. id: sha
  36. run: echo "short=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
  37. - name: Set image reference
  38. id: meta
  39. run: echo "image=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" >> "$GITHUB_OUTPUT"
  40. - name: Build and push demo image
  41. uses: docker/build-push-action@v6
  42. with:
  43. context: .
  44. push: true
  45. tags: |
  46. ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
  47. ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:demo-${{ steps.sha.outputs.short }}
  48. build-args: |
  49. APP_VERSION=demo-${{ steps.sha.outputs.short }}
  50. cache-from: type=gha,scope=demo
  51. cache-to: type=gha,scope=demo,mode=min
  52. deploy:
  53. needs: build
  54. runs-on: [self-hosted, Linux, X64, hetzner]
  55. steps:
  56. - name: Checkout demo branch
  57. uses: actions/checkout@v4
  58. with:
  59. ref: demo
  60. - name: Log in to GitHub Container Registry
  61. run: echo "${{ secrets.GHCR_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
  62. - name: Create deployment files
  63. env:
  64. VIRTUAL_HOST: "demo.torqvoice.com"
  65. DATABASE_URL: ${{ secrets.DEMO_DATABASE_URL }}
  66. BETTER_AUTH_SECRET: ${{ secrets.DEMO_BETTER_AUTH_SECRET }}
  67. # Optional. Next encrypts what a server action closes over with a key
  68. # made fresh by every build, so a tab left open across a deploy can
  69. # no longer be read by the new container. A fixed key keeps those tabs
  70. # working. 32 random bytes, base64: `openssl rand -base64 32`. Left
  71. # unset, the build's own key is used, as before.
  72. NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: ${{ secrets.DEMO_NEXT_SERVER_ACTIONS_ENCRYPTION_KEY }}
  73. APP_URL: "https://demo.torqvoice.com"
  74. DEMO_USER_EMAIL: "demo@torqvoice.com"
  75. DEMO_USER_PASSWORD: "demo"
  76. DATA_PATH: ${{ secrets.DATA_PATH }}
  77. POSTHOG_HOST: ${{ secrets.NEXT_PUBLIC_POSTHOG_HOST }}
  78. POSTHOG_KEY: ${{ secrets.POSTHOG_DEMO_KEY }}
  79. DEMO_IMAGE: ${{ needs.build.outputs.image }}
  80. run: |
  81. mkdir -p $HOME/torqvoice-deploy/demo
  82. cd $HOME/torqvoice-deploy/demo
  83. env | grep -E '^(VIRTUAL_HOST|DATA_PATH|DATABASE_URL|BETTER_AUTH_SECRET|APP_URL|DEMO_|POSTHOG_|NEXT_SERVER_ACTIONS_)' > .env
  84. cat > docker-compose.yml << 'COMPOSE'
  85. networks:
  86. proxy:
  87. external: true
  88. name: proxy
  89. services:
  90. torqvoice-app-demo:
  91. container_name: torqvoice-app-demo
  92. image: ${DEMO_IMAGE}:latest
  93. restart: unless-stopped
  94. volumes:
  95. - ${DATA_PATH}/torqvoice_demo_data:/app/data
  96. expose:
  97. - "3000"
  98. environment:
  99. VIRTUAL_HOST: ${VIRTUAL_HOST}
  100. VIRTUAL_PORT: "3000"
  101. LETSENCRYPT_HOST: ${VIRTUAL_HOST}
  102. DATABASE_URL: ${DATABASE_URL}
  103. BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
  104. NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: ${NEXT_SERVER_ACTIONS_ENCRYPTION_KEY}
  105. NEXT_PUBLIC_APP_URL: ${APP_URL}
  106. DEMO_MODE: "true"
  107. # The demo sits behind Cloudflare on the same origin as production,
  108. # which only accepts its edges; see deploy-prod.yml. Without this
  109. # every visitor shares the edge's rate-limit bucket, and the upload
  110. # and sign-in limits refuse strangers for each other.
  111. TRUST_CF_CONNECTING_IP: "true"
  112. DEMO_USER_EMAIL: ${DEMO_USER_EMAIL}
  113. DEMO_USER_PASSWORD: ${DEMO_USER_PASSWORD}
  114. POSTHOG_HOST: ${POSTHOG_HOST}
  115. POSTHOG_KEY: ${POSTHOG_KEY}
  116. networks:
  117. - proxy
  118. COMPOSE
  119. - name: Ensure data directory permissions
  120. run: |
  121. DATA_DIR="${{ secrets.DATA_PATH }}/torqvoice_demo_data"
  122. mkdir -p "$DATA_DIR"
  123. docker run --rm -v "$DATA_DIR":/data alpine chown -R 1001:1001 /data
  124. - name: Check free disk space
  125. run: |
  126. MIN_GB=5
  127. DOCKER_ROOT=$(docker info --format '{{.DockerRootDir}}' 2>/dev/null || echo /var/lib/docker)
  128. AVAIL_KB=$(df -Pk "$DOCKER_ROOT" | awk 'NR==2 {print $4}')
  129. AVAIL_GB=$(awk "BEGIN {printf \"%.1f\", $AVAIL_KB/1024/1024}")
  130. echo "Free space on $DOCKER_ROOT: ${AVAIL_GB} GB (minimum ${MIN_GB} GB)"
  131. if [ "$AVAIL_KB" -lt $((MIN_GB * 1024 * 1024)) ]; then
  132. echo "::error::Not enough disk space to pull image: ${AVAIL_GB} GB free, need ${MIN_GB} GB. Aborting deploy."
  133. exit 1
  134. fi
  135. - name: Deploy demo
  136. run: |
  137. cd $HOME/torqvoice-deploy/demo
  138. docker compose up -d --pull always
  139. - name: Health check
  140. run: |
  141. # Docker reports "running" as soon as the entrypoint starts, which is
  142. # before init-db.sh has applied migrations. The seed step below writes
  143. # to tables those migrations touch, so waiting on status alone let it
  144. # race ahead and fail on a column that did not exist yet. The health
  145. # route only answers once node server.js is up, i.e. after migrating.
  146. echo "Waiting for demo app to serve..."
  147. for i in $(seq 1 60); do
  148. STATUS=$(docker inspect --format='{{.State.Status}}' torqvoice-app-demo 2>/dev/null || echo "not found")
  149. case "$STATUS" in
  150. running|created|restarting) ;;
  151. *)
  152. echo "::error::Container is not running (status: $STATUS)"
  153. docker logs torqvoice-app-demo --tail 50
  154. exit 1
  155. ;;
  156. esac
  157. if docker exec torqvoice-app-demo \
  158. wget -q -O /dev/null http://127.0.0.1:3000/api/v1/health 2>/dev/null; then
  159. echo "Demo app is healthy!"
  160. exit 0
  161. fi
  162. echo "Not serving yet, retrying in 5s..."
  163. sleep 5
  164. done
  165. echo "::error::Demo app did not become healthy in time"
  166. docker logs torqvoice-app-demo --tail 50
  167. exit 1
  168. - name: Seed demo data
  169. # workflow_dispatch is the only trigger, so the old `push` clause was
  170. # dead. A boolean input is a real boolean in the `inputs` context.
  171. if: ${{ github.event_name == 'workflow_dispatch' && inputs.reseed }}
  172. run: |
  173. docker exec \
  174. -e DEMO_USER_EMAIL="demo@torqvoice.com" \
  175. -e DEMO_USER_PASSWORD="demo" \
  176. -e DATA_ROOT=/app/data \
  177. torqvoice-app-demo \
  178. npx tsx prisma/seed_dummy_data.ts
  179. - name: Prune old images and build cache
  180. run: docker image prune -f && docker builder prune -f