4
0

deploy-demo.yml 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  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. # A short hash of the server action key, so a new key rebuilds Next
  41. # instead of reusing a cached build (see the Dockerfile).
  42. - name: Fingerprint the server action key
  43. id: actions_key
  44. env:
  45. KEY: ${{ secrets.NEXT_SERVER_ACTIONS_BUILD_KEY }}
  46. run: echo "id=$(printf '%s' "$KEY" | sha256sum | cut -c1-12)" >> "$GITHUB_OUTPUT"
  47. - name: Build and push demo image
  48. uses: docker/build-push-action@v6
  49. with:
  50. context: .
  51. push: true
  52. tags: |
  53. ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
  54. ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:demo-${{ steps.sha.outputs.short }}
  55. build-args: |
  56. APP_VERSION=demo-${{ steps.sha.outputs.short }}
  57. NEXT_ACTIONS_FINGERPRINT=${{ steps.actions_key.outputs.id }}
  58. # Keeps server action IDs the same from build to build (see the Dockerfile).
  59. secrets: |
  60. next_actions_key=${{ secrets.NEXT_SERVER_ACTIONS_BUILD_KEY }}
  61. cache-from: type=gha,scope=demo
  62. cache-to: type=gha,scope=demo,mode=min
  63. deploy:
  64. needs: build
  65. runs-on: [self-hosted, Linux, X64, hetzner]
  66. steps:
  67. - name: Checkout demo branch
  68. uses: actions/checkout@v4
  69. with:
  70. ref: demo
  71. - name: Log in to GitHub Container Registry
  72. run: echo "${{ secrets.GHCR_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
  73. - name: Create deployment files
  74. env:
  75. VIRTUAL_HOST: "demo.torqvoice.com"
  76. DATABASE_URL: ${{ secrets.DEMO_DATABASE_URL }}
  77. BETTER_AUTH_SECRET: ${{ secrets.DEMO_BETTER_AUTH_SECRET }}
  78. # Optional. Next encrypts what a server action closes over with a key
  79. # made fresh by every build, so a tab left open across a deploy can
  80. # no longer be read by the new container. A fixed key keeps those tabs
  81. # working. 32 random bytes, base64: `openssl rand -base64 32`. Left
  82. # unset, the build's own key is used, as before.
  83. NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: ${{ secrets.DEMO_NEXT_SERVER_ACTIONS_ENCRYPTION_KEY }}
  84. APP_URL: "https://demo.torqvoice.com"
  85. DEMO_USER_EMAIL: "demo@torqvoice.com"
  86. DEMO_USER_PASSWORD: "demo"
  87. DATA_PATH: ${{ secrets.DATA_PATH }}
  88. POSTHOG_HOST: ${{ secrets.NEXT_PUBLIC_POSTHOG_HOST }}
  89. POSTHOG_KEY: ${{ secrets.POSTHOG_DEMO_KEY }}
  90. DEMO_IMAGE: ${{ needs.build.outputs.image }}
  91. run: |
  92. mkdir -p $HOME/torqvoice-deploy/demo
  93. cd $HOME/torqvoice-deploy/demo
  94. env | grep -E '^(VIRTUAL_HOST|DATA_PATH|DATABASE_URL|BETTER_AUTH_SECRET|APP_URL|DEMO_|POSTHOG_|NEXT_SERVER_ACTIONS_)' > .env
  95. cat > docker-compose.yml << 'COMPOSE'
  96. networks:
  97. proxy:
  98. external: true
  99. name: proxy
  100. services:
  101. torqvoice-app-demo:
  102. container_name: torqvoice-app-demo
  103. image: ${DEMO_IMAGE}:latest
  104. restart: unless-stopped
  105. volumes:
  106. - ${DATA_PATH}/torqvoice_demo_data:/app/data
  107. expose:
  108. - "3000"
  109. environment:
  110. VIRTUAL_HOST: ${VIRTUAL_HOST}
  111. VIRTUAL_PORT: "3000"
  112. LETSENCRYPT_HOST: ${VIRTUAL_HOST}
  113. DATABASE_URL: ${DATABASE_URL}
  114. BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
  115. NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: ${NEXT_SERVER_ACTIONS_ENCRYPTION_KEY}
  116. NEXT_PUBLIC_APP_URL: ${APP_URL}
  117. DEMO_MODE: "true"
  118. DEMO_USER_EMAIL: ${DEMO_USER_EMAIL}
  119. DEMO_USER_PASSWORD: ${DEMO_USER_PASSWORD}
  120. POSTHOG_HOST: ${POSTHOG_HOST}
  121. POSTHOG_KEY: ${POSTHOG_KEY}
  122. networks:
  123. - proxy
  124. COMPOSE
  125. - name: Ensure data directory permissions
  126. run: |
  127. DATA_DIR="${{ secrets.DATA_PATH }}/torqvoice_demo_data"
  128. mkdir -p "$DATA_DIR"
  129. docker run --rm -v "$DATA_DIR":/data alpine chown -R 1001:1001 /data
  130. - name: Check free disk space
  131. run: |
  132. MIN_GB=5
  133. DOCKER_ROOT=$(docker info --format '{{.DockerRootDir}}' 2>/dev/null || echo /var/lib/docker)
  134. AVAIL_KB=$(df -Pk "$DOCKER_ROOT" | awk 'NR==2 {print $4}')
  135. AVAIL_GB=$(awk "BEGIN {printf \"%.1f\", $AVAIL_KB/1024/1024}")
  136. echo "Free space on $DOCKER_ROOT: ${AVAIL_GB} GB (minimum ${MIN_GB} GB)"
  137. if [ "$AVAIL_KB" -lt $((MIN_GB * 1024 * 1024)) ]; then
  138. echo "::error::Not enough disk space to pull image: ${AVAIL_GB} GB free, need ${MIN_GB} GB. Aborting deploy."
  139. exit 1
  140. fi
  141. - name: Deploy demo
  142. run: |
  143. cd $HOME/torqvoice-deploy/demo
  144. docker compose up -d --pull always
  145. - name: Health check
  146. run: |
  147. echo "Waiting for the demo container to report healthy..."
  148. # Healthy is the image's own HEALTHCHECK passing: database, uploads and
  149. # background jobs (see the Dockerfile). An image from before it has no
  150. # health status, and then running is all there is to wait for.
  151. for i in $(seq 1 36); do
  152. STATUS=$(docker inspect --format='{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' torqvoice-app-demo 2>/dev/null || echo "not found")
  153. if [ "$STATUS" = "healthy" ] || [ "$STATUS" = "running" ]; then
  154. echo "Container is running!"
  155. exit 0
  156. fi
  157. echo "Status: $STATUS, retrying in 5s..."
  158. sleep 5
  159. done
  160. echo "::error::Container did not become healthy"
  161. docker logs torqvoice-app-demo --tail 50
  162. exit 1
  163. - name: Seed demo data
  164. if: ${{ github.event_name == 'push' || inputs.reseed }}
  165. run: |
  166. # The seed runs from /app/tools, beside the full Prisma client and
  167. # tsx the image keeps there for it (see Dockerfile).
  168. docker exec \
  169. -w /app/tools \
  170. -e DEMO_USER_EMAIL="demo@torqvoice.com" \
  171. -e DEMO_USER_PASSWORD="demo" \
  172. -e DATA_ROOT=/app/data \
  173. torqvoice-app-demo \
  174. npx tsx prisma/seed_dummy_data.ts
  175. - name: Prune old images and build cache
  176. run: docker image prune -f && docker builder prune -f