Initial bemo worker setup (#4017)
Sets up preview deploys etc. for bemo worker. There's enough going on here that I wanted to make it its own PR. I'll rework david's spike on top of it once it's landed. ### Change Type - [x] `internal` — Does not affect user-facing stuff - [x] `chore` — Updating dependencies, other boring stuff --------- Co-authored-by: David Sheldrick <d.j.sheldrick@gmail.com>
This commit is contained in:
parent
cafa0f5636
commit
57fb7a0650
55 changed files with 1278 additions and 719 deletions
54
.github/workflows/deploy-bemo.yml
vendored
Normal file
54
.github/workflows/deploy-bemo.yml
vendored
Normal file
|
@ -0,0 +1,54 @@
|
|||
name: Deploy bemo
|
||||
|
||||
# TODO: add some sort of production trigger
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
CI: 1
|
||||
PRINT_GITHUB_ANNOTATIONS: 1
|
||||
TLDRAW_ENV: ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }}
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy bemo to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }}
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest-16-cores-open
|
||||
environment: ${{ github.ref == 'refs/heads/production' && 'bemo-production' || 'bemo-canary' }}
|
||||
concurrency: bemo-${{ github.ref == 'refs/heads/production' && 'production' || github.ref }}
|
||||
|
||||
steps:
|
||||
- name: Notify initial start
|
||||
uses: MineBartekSA/discord-webhook@v2
|
||||
if: github.ref == 'refs/heads/production'
|
||||
with:
|
||||
webhook: ${{ secrets.DISCORD_DEPLOY_WEBHOOK_URL }}
|
||||
content: 'Preparing ${{ env.TLDRAW_ENV }} bemo deploy: ${{ github.event.head_commit.message }} by ${{ github.event.head_commit.author.name }}'
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- uses: ./.github/actions/setup
|
||||
|
||||
- name: Build types
|
||||
run: yarn build-types
|
||||
|
||||
- name: Deploy
|
||||
run: yarn tsx scripts/deploy-bemo.ts
|
||||
env:
|
||||
RELEASE_COMMIT_HASH: ${{ github.sha }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
DISCORD_DEPLOY_WEBHOOK_URL: ${{ secrets.DISCORD_DEPLOY_WEBHOOK_URL }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_BEMO_WORKER_DSN: ${{ secrets.SENTRY_BEMO_WORKER_DSN }}
|
|
@ -1,4 +1,4 @@
|
|||
name: Deploy
|
||||
name: Deploy .com
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
@ -17,11 +17,11 @@ defaults:
|
|||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }}
|
||||
name: Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }}
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest-16-cores-open
|
||||
environment: ${{ github.ref == 'refs/heads/production' && 'deploy-production' || 'deploy-staging' }}
|
||||
concurrency: ${{ github.ref == 'refs/heads/production' && 'deploy-production' || github.ref }}
|
||||
concurrency: dotcom-${{ github.ref == 'refs/heads/production' && 'deploy-production' || github.ref }}
|
||||
|
||||
steps:
|
||||
- name: Notify initial start
|
||||
|
@ -29,14 +29,7 @@ jobs:
|
|||
if: github.ref == 'refs/heads/production'
|
||||
with:
|
||||
webhook: ${{ secrets.DISCORD_DEPLOY_WEBHOOK_URL }}
|
||||
content: 'Preparing ${{ env.TLDRAW_ENV }} deploy: ${{ github.event.head_commit.message }} by ${{ github.event.head_commit.author.name }}'
|
||||
component: |
|
||||
{
|
||||
"type": 2,
|
||||
"style": 5,
|
||||
"label": "Open in GitHub",
|
||||
"url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
}
|
||||
content: 'Preparing ${{ env.TLDRAW_ENV }} dotcom deploy: ${{ github.event.head_commit.message }} by ${{ github.event.head_commit.author.name }}'
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3
|
||||
|
@ -49,7 +42,7 @@ jobs:
|
|||
run: yarn build-types
|
||||
|
||||
- name: Deploy
|
||||
run: yarn tsx scripts/deploy.ts
|
||||
run: yarn tsx scripts/deploy-dotcom.ts
|
||||
env:
|
||||
RELEASE_COMMIT_HASH: ${{ github.sha }}
|
||||
GH_TOKEN: ${{ github.token }}
|
2
apps/bemo-worker/.gitignore
vendored
Normal file
2
apps/bemo-worker/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
build
|
||||
.wrangler
|
38
apps/bemo-worker/README.md
Normal file
38
apps/bemo-worker/README.md
Normal file
|
@ -0,0 +1,38 @@
|
|||
# bemo-worker
|
||||
|
||||
```
|
||||
⠀⠀⠀⠀⠀⠀⢤⡴⠶⠷⠶⠶⠾⠷⠻⠶⠷⠿⠾⠶⠷⠿⠳⠷⠾⠾⠷⠷⠾⠾⠷⠿⠶⠿⠞⠷⠶⠷⢦⣤⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⢠⡼⠟⠋⠤⣤⠐⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉⠛⠻⣤⡤⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⢸⡧⠀⠁⡀⢀⠉⠙⠠⣄⣀⠀⠀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣘⣛⣧⣤⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⢸⡇⠐⠠⠀⣠⣤⡘⠀⠌⡈⣷⡶⠛⠋⠛⠙⠋⠛⠙⠋⠛⠙⠋⠛⠙⠋⠛⠙⠋⠛⠙⠋⠛⠙⠋⠛⠙⠋⠙⠉⠋⠛⠋⣶⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⢸⡧⢐⣶⣶⠙⠋⠃⣴⣮⠁⣿⠀⠀⠀⠀⢠⣄⣤⣠⣄⣤⣤⣤⣠⣤⣤⣤⣠⣤⣤⣤⣠⣤⣤⣤⣄⣤⣠⣄⣠⡄⠀⠀⠀⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⢸⡗⢸⠏⢁⡶⣶⡶⠏⠉⡀⣿⠀⠀⠐⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⠀⠐⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⢸⡧⠰⣿⡾⠁⠉⠀⣾⣿⠄⣿⠀⠀⠂⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⠀⠀⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⢸⡧⠐⠉⡁⣰⣶⡆⠉⠉⠀⣿⠀⠀⡀⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⠀⠠⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⢸⡇⢈⠐⠀⠀⠉⡀⢀⠁⠂⣿⠀⠀⠀⢸⡇⠀⠀⠀⠀⠀⠛⣤⡼⠋⠀⠀⠀⠀⠀⠀⠀⠀⠛⢣⣤⠛⠀⠀⠀⢸⡇⠀⠀⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⢸⣏⢠⣷⣾⣾⣤⣠⡀⠂⡁⣿⠀⠀⠈⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠶⣆⣀⣀⣰⠶⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⠀⢀⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⢸⡇⢸⣿⣿⣿⢿⣿⡃⠄⡀⣿⠀⠀⠐⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠉⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⠀⠀⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⢸⡇⠸⣟⣿⣻⣿⣿⡄⠀⠄⣿⠀⠀⠠⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⠀⠀⣿⠀⠀⠀⠀⠀⠀⣤⣿⣿⣤
|
||||
⠀⠀⠀⠀⢸⡗⠰⣿⣿⣿⣿⣾⠂⠄⠂⣿⠀⠀⢀⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⠀⠈⣿⠀⠀⠀⠀⠀⠀⣿⠀⢸⣯
|
||||
⠀⠀⠀⠀⢸⡏⠘⠉⣿⣿⣿⡿⠆⠀⡁⣿⠀⠀⠀⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⠀⠀⣿⠀⠀⠀⠀⠀⠀⣿⠀⢸⣧
|
||||
⠀⠀⠀⠀⢸⣇⠨⣿⣿⣿⣿⣿⡁⠂⠄⣿⠀⠀⠂⠸⢧⣠⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⡼⠇⠀⠈⣿⠀⠀⠀⠀⠀⠀⣿⠀⢸⡿
|
||||
⠀⠀⠀⠀⢸⡧⠘⠉⠙⠿⠻⠛⠂⢀⠂⣿⠀⠀⢀⠀⠀⢁⢈⡈⣁⢈⡁⣈⢁⡈⢁⠈⠁⠁⠈⠈⠀⠁⠈⠁⠉⠈⠁⠈⠀⠁⠀⠀⠠⣿⣤⠀⠀⠀⠀⠀⣿⡀⣸⣿
|
||||
⠀⠀⠀⠀⢸⣇⣠⣢⣷⣶⣶⣦⡑⠀⠄⣿⠀⠀⠀⢰⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⠀⠀⠠⠀⠀⢀⣶⣶⠀⠀⠄⠐⠀⠐⣿⣼⠟⢣⣤⣤⠟⠁⣰⠿⠀
|
||||
⠀⣀⣰⣶⠾⠛⠛⠙⠛⠉⣿⣿⡀⠐⡀⣿⠀⠀⠄⠘⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠉⠀⠀⢀⠠⠀⠘⠛⠛⠁⠀⠀⠀⠄⠈⣿⠙⠶⢧⣀⣀⣰⡾⠋⠀⠀
|
||||
⣶⠋⠉⣘⣟⢿⣻⣟⡻⠻⠿⣿⠀⠄⡀⣿⠀⠀⠀⠀⠀⠀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡀⠈⠀⠀⠀⡀⠀⠀⠀⡀⠐⠀⢀⠀⣿⠀⠀⠈⠉⠉⠉⠁⠀⠀⠀
|
||||
⠛⣦⣤⣤⣤⣤⣤⣤⣤⣤⠀⢹⣟⠀⡀⣿⠀⠀⠈⢀⣀⣻⡏⠉⣇⣀⡐⠀⠀⠄⠀⠀⠂⠀⢀⠀⣠⡼⠧⣄⠀⣠⣦⠄⠀⢀⠠⠀⠀⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⢸⡇⠠⠐⠠⠀⠛⠛⡀⠄⡀⣿⠀⠀⠠⢼⣇⣀⡀⢀⣀⣸⠇⠀⠀⠀⠀⠂⠀⠠⠀⠘⠛⠒⠒⠚⠻⣿⣂⠼⡇⠀⠀⢀⠠⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⢸⡇⠠⠁⠂⠡⠈⡀⠄⠂⠄⣿⠀⠀⠀⠀⠀⠹⢇⣠⡏⠀⠀⠀⠀⠁⠀⡀⠐⠀⠀⠀⣠⣶⣿⣿⣷⣬⡉⠁⠀⠀⢀⠀⠀⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠸⢷⣄⠂⣁⠂⠡⢀⠐⡈⠀⣿⠀⠀⠈⠀⠀⢂⣀⡠⠀⠀⠐⠀⠁⡀⠄⠀⠀⠀⠐⠀⣿⣿⣿⣿⣷⣿⡇⠀⠀⠠⠀⠀⠠⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠘⢳⣤⡄⢁⠂⠐⡀⠁⣿⠀⠀⢀⠘⠛⠛⠛⠃⠀⠘⠛⠛⠛⠛⠀⠀⠀⠁⠀⠀⠈⠻⠿⠿⠿⠋⠀⠀⢀⠀⠀⠐⠀⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠛⠛⡶⢦⣀⣄⣿⠀⠀⡀⠀⠠⠀⠀⠀⡀⠀⢀⠀⢀⠀⠀⡀⠂⠀⠈⠀⡀⠀⠀⠀⠀⠀⠀⠄⠀⠀⠈⠀⢀⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠁⠘⠛⢻⣿⣤⡀⣁⣀⡀⣀⢂⣀⣁⣀⣈⡀⣈⣀⣀⣁⣀⡐⣀⣀⢁⡀⣂⢀⣀⢂⣀⣀⢀⢂⣀⣠⣼⠛⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠛⠛⠛⠛⠛⢻⣿⣻⣿⣛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⣻⣟⣿⣿⣿⡛⠛⠛⠛⠛⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⣿⡿⣿⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣻⣿⣿⣿⣯⣷⡗⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⣿⢻⣧⠀⠀⠀⠀⠀⠀⠀⠀⢰⡞⣿⣿⡶⣯⣽⡿⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⢸⣿⠀⠀⠀⠀⠀⠀⠀⠀⠘⠛⠛⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣏⣿⠛⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣶⡟⢩⣟⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠿⣧⣾⣿⣶⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠁⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
```
|
49
apps/bemo-worker/package.json
Normal file
49
apps/bemo-worker/package.json
Normal file
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"name": "@tldraw/bemo-worker",
|
||||
"description": "A tiny little drawing app (merge server).",
|
||||
"version": "2.0.0-alpha.11",
|
||||
"private": true,
|
||||
"author": {
|
||||
"name": "tldraw GB Ltd.",
|
||||
"email": "hello@tldraw.com"
|
||||
},
|
||||
"main": "./src/worker.ts",
|
||||
"/* GOTCHA */": "files will include ./dist and index.d.ts by default, add any others you want to include in here",
|
||||
"files": [],
|
||||
"scripts": {
|
||||
"dev": "yarn run -T tsx ../../scripts/workers/dev.ts",
|
||||
"test-ci": "lazy inherit",
|
||||
"test": "yarn run -T jest",
|
||||
"test-coverage": "lazy inherit",
|
||||
"check-bundle-size": "yarn run -T tsx ../../scripts/check-worker-bundle.ts --entry src/worker.ts --size-limit-bytes 350000",
|
||||
"lint": "yarn run -T tsx ../../scripts/lint.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tldraw/dotcom-shared": "workspace:*",
|
||||
"@tldraw/store": "workspace:*",
|
||||
"@tldraw/tlschema": "workspace:*",
|
||||
"@tldraw/tlsync": "workspace:*",
|
||||
"@tldraw/utils": "workspace:*",
|
||||
"itty-router": "^4.0.13",
|
||||
"nanoid": "4.0.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"toucan-js": "^3.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20240620.0",
|
||||
"esbuild": "^0.21.5",
|
||||
"lazyrepo": "0.0.0-alpha.27",
|
||||
"typescript": "^5.3.3",
|
||||
"wrangler": "3.62.0"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "config/jest/node",
|
||||
"moduleNameMapper": {
|
||||
"^~(.*)": "<rootDir>/src/$1"
|
||||
},
|
||||
"transformIgnorePatterns": [
|
||||
"node_modules/(?!(nanoid|escape-string-regexp)/)"
|
||||
]
|
||||
}
|
||||
}
|
38
apps/bemo-worker/src/BemoDO.ts
Normal file
38
apps/bemo-worker/src/BemoDO.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
import { DurableObject } from 'cloudflare:workers'
|
||||
import { Router } from 'itty-router'
|
||||
import { createSentry } from './sentry'
|
||||
import { Environment } from './types'
|
||||
|
||||
export class BemoDO extends DurableObject<Environment> {
|
||||
private reportError(e: unknown, request?: Request) {
|
||||
const sentry = createSentry(this.ctx, this.env, request)
|
||||
console.error(e)
|
||||
// eslint-disable-next-line deprecation/deprecation
|
||||
sentry.captureException(e)
|
||||
}
|
||||
|
||||
private readonly router = Router()
|
||||
.get('/do', async () => {
|
||||
return Response.json({ message: 'Hello from a durable object!' })
|
||||
})
|
||||
.get('/do/error', async () => {
|
||||
this.doAnError()
|
||||
})
|
||||
.all('*', async () => new Response('Not found', { status: 404 }))
|
||||
|
||||
private doAnError() {
|
||||
throw new Error('this is an error from a DO')
|
||||
}
|
||||
|
||||
override async fetch(request: Request<unknown, CfProperties<unknown>>): Promise<Response> {
|
||||
try {
|
||||
return await this.router.handle(request)
|
||||
} catch (error) {
|
||||
this.reportError(error, request)
|
||||
return new Response('Something went wrong', {
|
||||
status: 500,
|
||||
statusText: 'Internal Server Error',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
21
apps/bemo-worker/src/sentry.ts
Normal file
21
apps/bemo-worker/src/sentry.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { Toucan } from 'toucan-js'
|
||||
import { Environment } from './types'
|
||||
|
||||
interface Context {
|
||||
waitUntil: ExecutionContext['waitUntil']
|
||||
request?: Request
|
||||
}
|
||||
|
||||
export function createSentry(ctx: Context, env: Environment, request?: Request) {
|
||||
return new Toucan({
|
||||
dsn: env.SENTRY_DSN,
|
||||
release: `${env.WORKER_NAME}.${env.CF_VERSION_METADATA.id}`,
|
||||
environment: env.WORKER_NAME,
|
||||
context: ctx,
|
||||
request,
|
||||
requestDataOptions: {
|
||||
allowedHeaders: ['user-agent'],
|
||||
allowedSearchParams: /(.*)/,
|
||||
},
|
||||
})
|
||||
}
|
12
apps/bemo-worker/src/types.ts
Normal file
12
apps/bemo-worker/src/types.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { BemoDO } from './BemoDO'
|
||||
|
||||
export interface Environment {
|
||||
// bindings
|
||||
BEMO_DO: DurableObjectNamespace<BemoDO>
|
||||
|
||||
TLDRAW_ENV: string | undefined
|
||||
SENTRY_DSN: string | undefined
|
||||
IS_LOCAL: string | undefined
|
||||
WORKER_NAME: string | undefined
|
||||
CF_VERSION_METADATA: WorkerVersionMetadata
|
||||
}
|
5
apps/bemo-worker/src/worker.test.ts
Normal file
5
apps/bemo-worker/src/worker.test.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
describe('worker', () => {
|
||||
it('works', () => {
|
||||
// blank test to make ci happy
|
||||
})
|
||||
})
|
37
apps/bemo-worker/src/worker.ts
Normal file
37
apps/bemo-worker/src/worker.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
/// <reference no-default-lib="true"/>
|
||||
/// <reference types="@cloudflare/workers-types" />
|
||||
|
||||
import { WorkerEntrypoint } from 'cloudflare:workers'
|
||||
import { Router, createCors } from 'itty-router'
|
||||
import { createSentry } from './sentry'
|
||||
import { Environment } from './types'
|
||||
|
||||
export { BemoDO } from './BemoDO'
|
||||
|
||||
const cors = createCors({ origins: ['*'] })
|
||||
|
||||
export default class Worker extends WorkerEntrypoint<Environment> {
|
||||
private readonly router = Router()
|
||||
.all('*', cors.preflight)
|
||||
.get('/do', async (request) => {
|
||||
const bemo = this.env.BEMO_DO.get(this.env.BEMO_DO.idFromName('bemo-do'))
|
||||
const message = await (await bemo.fetch(request)).json()
|
||||
return Response.json(message)
|
||||
})
|
||||
.all('*', async () => new Response('Not found', { status: 404 }))
|
||||
|
||||
override async fetch(request: Request): Promise<Response> {
|
||||
try {
|
||||
return await this.router.handle(request).then(cors.corsify)
|
||||
} catch (error) {
|
||||
const sentry = createSentry(this.ctx, this.env, request)
|
||||
console.error(error)
|
||||
// eslint-disable-next-line deprecation/deprecation
|
||||
sentry.captureException(error)
|
||||
return new Response('Something went wrong', {
|
||||
status: 500,
|
||||
statusText: 'Internal Server Error',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
26
apps/bemo-worker/tsconfig.json
Normal file
26
apps/bemo-worker/tsconfig.json
Normal file
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"extends": "../../config/tsconfig.base.json",
|
||||
"include": ["src", "scripts"],
|
||||
"exclude": ["node_modules", "dist", ".tsbuild*"],
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"emitDeclarationOnly": false
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "../../packages/dotcom-shared"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/store"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/tlschema"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/tlsync"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/utils"
|
||||
}
|
||||
]
|
||||
}
|
79
apps/bemo-worker/wrangler.toml
Normal file
79
apps/bemo-worker/wrangler.toml
Normal file
|
@ -0,0 +1,79 @@
|
|||
main = "src/worker.ts"
|
||||
compatibility_date = "2024-06-25"
|
||||
upload_source_maps = true
|
||||
|
||||
[dev]
|
||||
port = 8989
|
||||
ip = "0.0.0.0"
|
||||
|
||||
# these migrations are append-only. you can't change them. if you do need to change something, do so
|
||||
# by creating new migrations
|
||||
[[migrations]]
|
||||
tag = "v1" # Should be unique for each entry
|
||||
new_classes = ["BemoDO"]
|
||||
|
||||
|
||||
#################### Environment names ####################
|
||||
# dev should never actually get deployed anywhere
|
||||
[env.dev]
|
||||
name = "dev-bemo"
|
||||
|
||||
# we don't have a hard-coded name for preview. we instead have to generate it at build time and append it to this file.
|
||||
|
||||
# staging is the same as a preview on main:
|
||||
[env.staging]
|
||||
name = "canary-bemo"
|
||||
routes = [
|
||||
{ pattern = "canary-demo.tldraw.xyz", custom_domain = true }
|
||||
]
|
||||
|
||||
# production gets the proper name
|
||||
[env.production]
|
||||
name = "production-bemo"
|
||||
routes = [
|
||||
{ pattern = "demo.tldraw.xyz", custom_domain = true }
|
||||
]
|
||||
|
||||
#################### Durable objects ####################
|
||||
# durable objects have the same configuration in all environments:
|
||||
|
||||
[durable_objects]
|
||||
bindings = [
|
||||
{ name = "BEMO_DO", class_name = "BemoDO" },
|
||||
]
|
||||
|
||||
[env.dev.durable_objects]
|
||||
bindings = [
|
||||
{ name = "BEMO_DO", class_name = "BemoDO" },
|
||||
]
|
||||
|
||||
[env.preview.durable_objects]
|
||||
bindings = [
|
||||
{ name = "BEMO_DO", class_name = "BemoDO" },
|
||||
]
|
||||
|
||||
[env.staging.durable_objects]
|
||||
bindings = [
|
||||
{ name = "BEMO_DO", class_name = "BemoDO" },
|
||||
]
|
||||
|
||||
[env.production.durable_objects]
|
||||
bindings = [
|
||||
{ name = "BEMO_DO", class_name = "BemoDO" },
|
||||
]
|
||||
|
||||
#################### Version metadata ####################
|
||||
[version_metadata]
|
||||
binding = "CF_VERSION_METADATA"
|
||||
|
||||
[env.dev.version_metadata]
|
||||
binding = "CF_VERSION_METADATA"
|
||||
|
||||
[env.preview.version_metadata]
|
||||
binding = "CF_VERSION_METADATA"
|
||||
|
||||
[env.staging.version_metadata]
|
||||
binding = "CF_VERSION_METADATA"
|
||||
|
||||
[env.production.version_metadata]
|
||||
binding = "CF_VERSION_METADATA"
|
|
@ -23,7 +23,7 @@
|
|||
"@cloudflare/workers-types": "^4.20240620.0",
|
||||
"@types/ws": "^8.5.9",
|
||||
"lazyrepo": "0.0.0-alpha.27",
|
||||
"wrangler": "3.61.0"
|
||||
"wrangler": "3.62.0"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "config/jest/node",
|
||||
|
|
|
@ -26,6 +26,9 @@ preview_bucket_name = 'uploads-preview'
|
|||
type = "analytics_engine"
|
||||
name = "MEASURE"
|
||||
|
||||
[[env.staging.analytics_engine_datasets]]
|
||||
binding = "MEASURE"
|
||||
|
||||
|
||||
# production settings
|
||||
[env.production]
|
||||
|
@ -45,7 +48,13 @@ preview_bucket_name = 'uploads-preview'
|
|||
type = "analytics_engine"
|
||||
name = "MEASURE"
|
||||
|
||||
[[env.production.analytics_engine_datasets]]
|
||||
binding = "MEASURE"
|
||||
|
||||
[[env.preview.r2_buckets]]
|
||||
binding = 'UPLOADS'
|
||||
bucket_name = 'uploads'
|
||||
preview_bucket_name = 'uploads-preview'
|
||||
preview_bucket_name = 'uploads-preview'
|
||||
|
||||
[[env.preview.analytics_engine_datasets]]
|
||||
binding = "MEASURE"
|
|
@ -7,18 +7,15 @@
|
|||
"name": "tldraw GB Ltd.",
|
||||
"email": "hello@tldraw.com"
|
||||
},
|
||||
"main": "./src/lib/worker.ts",
|
||||
"main": "./src/worker.ts",
|
||||
"/* GOTCHA */": "files will include ./dist and index.d.ts by default, add any others you want to include in here",
|
||||
"files": [],
|
||||
"scripts": {
|
||||
"dev": "concurrently --kill-others yarn:dev-cron yarn:dev-wrangler yarn:report-size",
|
||||
"dev-cron": "yarn run -T tsx ./scripts/cron.ts",
|
||||
"dev-wrangler": "yarn run -T tsx ./scripts/dev-wrap.ts",
|
||||
"report-size": "node scripts/report-size.js",
|
||||
"dev": "yarn run -T tsx ../../scripts/workers/dev.ts",
|
||||
"test-ci": "lazy inherit",
|
||||
"test": "yarn run -T jest",
|
||||
"test-coverage": "lazy inherit",
|
||||
"check-bundle-size": "yarn run -T tsx ../../scripts/check-worker-bundle.ts --entry src/lib/worker.ts --size-limit-bytes 350000",
|
||||
"check-bundle-size": "yarn run -T tsx ../../scripts/check-worker-bundle.ts --entry src/worker.ts --size-limit-bytes 350000",
|
||||
"lint": "yarn run -T tsx ../../scripts/lint.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
|
@ -29,21 +26,18 @@
|
|||
"@tldraw/tlschema": "workspace:*",
|
||||
"@tldraw/tlsync": "workspace:*",
|
||||
"@tldraw/utils": "workspace:*",
|
||||
"esbuild": "^0.18.4",
|
||||
"itty-router": "^4.0.13",
|
||||
"nanoid": "4.0.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"toucan-js": "^2.7.0"
|
||||
"toucan-js": "^3.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20240620.0",
|
||||
"concurrently": "^8.2.2",
|
||||
"esbuild": "^0.21.5",
|
||||
"lazyrepo": "0.0.0-alpha.27",
|
||||
"picocolors": "^1.0.0",
|
||||
"typescript": "^5.3.3",
|
||||
"wrangler": "3.61.0"
|
||||
"wrangler": "3.62.0"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "config/jest/node",
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
const CRON_INTERVAL_MS = 10_000
|
||||
|
||||
setInterval(async () => {
|
||||
try {
|
||||
await fetch('http://127.0.0.1:8787/__scheduled')
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Error triggering cron:', err)
|
||||
}
|
||||
}, CRON_INTERVAL_MS)
|
|
@ -1,45 +0,0 @@
|
|||
/* eslint-disable no-undef */
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const { spawn } = require('child_process')
|
||||
const colors = require('picocolors')
|
||||
|
||||
class Monitor {
|
||||
lastLineTime = Date.now()
|
||||
nextTick = 0
|
||||
|
||||
size = 0
|
||||
|
||||
start() {
|
||||
console.log('Spawning')
|
||||
const proc = spawn('npx', ['esbuild', 'src/lib/worker.ts', '--bundle', '--minify', '--watch'])
|
||||
// listen for lines on stdin
|
||||
proc.stdout.on('data', (data) => {
|
||||
this.size += data.length
|
||||
this.lastLineTime = Date.now()
|
||||
clearTimeout(this.nextTick)
|
||||
this.nextTick = setTimeout(() => {
|
||||
console.log(
|
||||
colors.bold(colors.yellow('dotcom-worker')),
|
||||
'is roughly',
|
||||
colors.bold(colors.cyan(Math.floor(this.size / 1024) + 'kb')),
|
||||
'(minified)\n'
|
||||
)
|
||||
this.size = 0
|
||||
}, 10)
|
||||
})
|
||||
process.on('SIGINT', () => {
|
||||
console.log('Int')
|
||||
proc.kill()
|
||||
})
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('Term')
|
||||
proc.kill()
|
||||
})
|
||||
process.on('exit', () => {
|
||||
console.log('Exiting')
|
||||
proc.kill()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
new Monitor().start()
|
|
@ -18,7 +18,7 @@ import {
|
|||
} from '@tldraw/tlsync'
|
||||
import { assert, assertExists, exhaustiveSwitchError } from '@tldraw/utils'
|
||||
import { IRequest, Router } from 'itty-router'
|
||||
import Toucan from 'toucan-js'
|
||||
import { Toucan } from 'toucan-js'
|
||||
import { AlarmScheduler } from './AlarmScheduler'
|
||||
import { PERSIST_INTERVAL_MS } from './config'
|
||||
import { getR2KeyForRoom } from './r2'
|
||||
|
@ -218,21 +218,17 @@ export class TLDrawDurableObject {
|
|||
const sentry = new Toucan({
|
||||
dsn: this.sentryDSN,
|
||||
request: req,
|
||||
allowedHeaders: ['user-agent'],
|
||||
allowedSearchParams: /(.*)/,
|
||||
requestDataOptions: {
|
||||
allowedHeaders: ['user-agent'],
|
||||
allowedSearchParams: /(.*)/,
|
||||
},
|
||||
})
|
||||
|
||||
try {
|
||||
return await this.router.handle(req).catch((err) => {
|
||||
console.error(err)
|
||||
sentry.captureException(err)
|
||||
|
||||
return new Response('Something went wrong', {
|
||||
status: 500,
|
||||
statusText: 'Internal Server Error',
|
||||
})
|
||||
})
|
||||
return await this.router.handle(req)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
// eslint-disable-next-line deprecation/deprecation
|
||||
sentry.captureException(err)
|
||||
return new Response('Something went wrong', {
|
||||
status: 500,
|
|
@ -7,7 +7,7 @@ import {
|
|||
ROOM_PREFIX,
|
||||
} from '@tldraw/dotcom-shared'
|
||||
import { Router, createCors } from 'itty-router'
|
||||
import Toucan from 'toucan-js'
|
||||
import { Toucan } from 'toucan-js'
|
||||
import { createRoom } from './routes/createRoom'
|
||||
import { createRoomSnapshot } from './routes/createRoomSnapshot'
|
||||
import { forwardRoomRequest } from './routes/forwardRoomRequest'
|
||||
|
@ -51,14 +51,17 @@ const Worker = {
|
|||
dsn: env.SENTRY_DSN,
|
||||
context, // Includes 'waitUntil', which is essential for Sentry logs to be delivered. Modules workers do not include 'request' in context -- you'll need to set it separately.
|
||||
request, // request is not included in 'context', so we set it here.
|
||||
allowedHeaders: ['user-agent'],
|
||||
allowedSearchParams: /(.*)/,
|
||||
requestDataOptions: {
|
||||
allowedHeaders: ['user-agent'],
|
||||
allowedSearchParams: /(.*)/,
|
||||
},
|
||||
})
|
||||
|
||||
return router
|
||||
.handle(request, env, context)
|
||||
.catch((err) => {
|
||||
console.error(err)
|
||||
// eslint-disable-next-line deprecation/deprecation
|
||||
sentry.captureException(err)
|
||||
|
||||
return new Response('Something went wrong', {
|
|
@ -1,4 +1,4 @@
|
|||
main = "src/lib/worker.ts"
|
||||
main = "src/worker.ts"
|
||||
compatibility_date = "2024-06-19"
|
||||
|
||||
[dev]
|
||||
|
|
|
@ -22,6 +22,7 @@ Sentry.init({
|
|||
tracesSampleRate: 1.0,
|
||||
release: sentryReleaseName,
|
||||
environment: env,
|
||||
// eslint-disable-next-line deprecation/deprecation
|
||||
integrations: [new ExtraErrorData({ depth: 10 }) as any],
|
||||
// ...
|
||||
// Note: if you want to override the automatic release value, do not set a
|
||||
|
|
|
@ -16,8 +16,8 @@
|
|||
"@cloudflare/workers-types": "^4.20240620.0",
|
||||
"@types/node": "~20.11",
|
||||
"discord-api-types": "^0.37.67",
|
||||
"esbuild": "^0.18.4",
|
||||
"esbuild": "^0.21.5",
|
||||
"typescript": "^5.3.3",
|
||||
"wrangler": "3.61.0"
|
||||
"wrangler": "3.62.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
"concurrently": "^8.2.2",
|
||||
"create-serve": "1.0.1",
|
||||
"dotenv": "^16.3.1",
|
||||
"esbuild": "^0.18.4",
|
||||
"esbuild": "^0.21.5",
|
||||
"fs-extra": "^11.1.0",
|
||||
"lazyrepo": "0.0.0-alpha.27",
|
||||
"react": "^18.2.0",
|
||||
|
|
|
@ -137,7 +137,7 @@
|
|||
"@typescript-eslint/eslint-plugin": "^5.57.0",
|
||||
"@typescript-eslint/parser": "^5.57.0",
|
||||
"assert": "^2.0.0",
|
||||
"esbuild": "^0.18.4",
|
||||
"esbuild": "^0.21.5",
|
||||
"eslint": "^8.37.0",
|
||||
"fs-extra": "^11.1.0",
|
||||
"lazyrepo": "0.0.0-alpha.27",
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
"clean": "scripts/clean.sh",
|
||||
"postinstall": "husky install && yarn refresh-assets",
|
||||
"refresh-assets": "lazy refresh-assets",
|
||||
"dev": "LAZYREPO_PRETTY_OUTPUT=0 lazy run dev --filter='apps/examples' --filter='packages/tldraw'",
|
||||
"dev": "LAZYREPO_PRETTY_OUTPUT=0 lazy run dev --filter='apps/examples' --filter='packages/tldraw' --filter='apps/bemo-worker'",
|
||||
"dev-vscode": "code ./apps/vscode/extension && lazy run dev --filter='apps/vscode/{extension,editor}'",
|
||||
"dev-app": "LAZYREPO_PRETTY_OUTPUT=0 lazy run dev --filter='apps/{dotcom,dotcom-asset-upload,dotcom-worker}' --filter='packages/tldraw'",
|
||||
"dev-docs": "LAZYREPO_PRETTY_OUTPUT=0 lazy run dev --filter='apps/docs'",
|
||||
|
@ -120,6 +120,7 @@
|
|||
"@sentry/cli": "^2.25.0",
|
||||
"@yarnpkg/types": "^4.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"esbuild": "^0.21.5",
|
||||
"mime": "^4.0.3",
|
||||
"purgecss": "^5.0.0",
|
||||
"svgo": "^3.0.2"
|
||||
|
|
|
@ -41,7 +41,7 @@ export const assert: (value: unknown, message?: string) => asserts value = omitF
|
|||
|
||||
/** @internal */
|
||||
export const assertExists = omitFromStackTrace(<T>(value: T, message?: string): NonNullable<T> => {
|
||||
// note that value == null is equivilent to value === null || value === undefined
|
||||
// note that value == null is equivalent to value === null || value === undefined
|
||||
if (value == null) {
|
||||
throw new Error(message ?? 'value must be defined')
|
||||
}
|
||||
|
|
120
scripts/deploy-bemo.ts
Normal file
120
scripts/deploy-bemo.ts
Normal file
|
@ -0,0 +1,120 @@
|
|||
import assert from 'assert'
|
||||
import { readFileSync } from 'fs'
|
||||
import path, { join } from 'path'
|
||||
import toml from 'toml'
|
||||
import {
|
||||
createGithubDeployment,
|
||||
getDeployInfo,
|
||||
setWranglerPreviewConfig,
|
||||
wranglerDeploy,
|
||||
} from './lib/deploy'
|
||||
import { Discord } from './lib/discord'
|
||||
import { exec } from './lib/exec'
|
||||
import { makeEnv } from './lib/makeEnv'
|
||||
import { nicelog } from './lib/nicelog'
|
||||
|
||||
const workerDir = path.relative(process.cwd(), path.resolve(__dirname, '../apps/bemo-worker'))
|
||||
|
||||
// Do not use `process.env` directly in this script. Add your variable to `makeEnv` and use it via
|
||||
// `env` instead. This makes sure that all required env vars are present.
|
||||
const env = makeEnv([
|
||||
'CLOUDFLARE_ACCOUNT_ID',
|
||||
'CLOUDFLARE_API_TOKEN',
|
||||
'DISCORD_DEPLOY_WEBHOOK_URL',
|
||||
'RELEASE_COMMIT_HASH',
|
||||
'TLDRAW_ENV',
|
||||
'GH_TOKEN',
|
||||
'SENTRY_AUTH_TOKEN',
|
||||
'SENTRY_BEMO_WORKER_DSN',
|
||||
])
|
||||
|
||||
const discord = new Discord({
|
||||
webhookUrl: env.DISCORD_DEPLOY_WEBHOOK_URL,
|
||||
shouldNotify: env.TLDRAW_ENV === 'production',
|
||||
totalSteps: 3,
|
||||
})
|
||||
|
||||
const { previewId, sha } = getDeployInfo()
|
||||
|
||||
async function main() {
|
||||
assert(
|
||||
env.TLDRAW_ENV === 'staging' || env.TLDRAW_ENV === 'production' || env.TLDRAW_ENV === 'preview',
|
||||
'TLDRAW_ENV must be staging or production or preview'
|
||||
)
|
||||
|
||||
await discord.message(`--- **${env.TLDRAW_ENV} bemo deploy pre-flight** ---`)
|
||||
|
||||
await discord.step('setting up deploy', async () => {
|
||||
await exec('yarn', ['lazy', 'prebuild'])
|
||||
})
|
||||
|
||||
await discord.step('cloudflare deploy dry run', async () => {
|
||||
await deployBemoWorker({ dryRun: true })
|
||||
})
|
||||
|
||||
// --- point of no return! do the deploy for real --- //
|
||||
|
||||
await discord.message(`--- **pre-flight complete, starting real bemo deploy** ---`)
|
||||
|
||||
// 2. deploy the cloudflare workers:
|
||||
await discord.step('deploying bemo-worker to cloudflare', async () => {
|
||||
await deployBemoWorker({ dryRun: false })
|
||||
})
|
||||
|
||||
// we set the domain in the wrangler.toml file since it's managed by cloudflare
|
||||
const domain = toml.parse(readFileSync(join(workerDir, 'wrangler.toml')).toString())?.env[
|
||||
env.TLDRAW_ENV
|
||||
]?.routes?.[0]?.pattern
|
||||
if (!domain) {
|
||||
throw new Error('Could not find the domain in wrangler.toml')
|
||||
}
|
||||
|
||||
const deploymentUrl = `https://${domain}`
|
||||
|
||||
nicelog('Creating deployment for', deploymentUrl)
|
||||
await createGithubDeployment(env, {
|
||||
app: 'bemo',
|
||||
deploymentUrl,
|
||||
sha,
|
||||
})
|
||||
|
||||
await discord.message(`**Deploy complete!**`)
|
||||
}
|
||||
|
||||
let didUpdateBemoWorker = false
|
||||
async function deployBemoWorker({ dryRun }: { dryRun: boolean }) {
|
||||
const workerId = `${previewId ?? env.TLDRAW_ENV}-bemo`
|
||||
if (previewId && !didUpdateBemoWorker) {
|
||||
await setWranglerPreviewConfig(workerDir, {
|
||||
name: workerId,
|
||||
customDomain: `${previewId}-demo.tldraw.xyz`,
|
||||
})
|
||||
didUpdateBemoWorker = true
|
||||
}
|
||||
|
||||
await wranglerDeploy({
|
||||
location: workerDir,
|
||||
dryRun,
|
||||
env: env.TLDRAW_ENV,
|
||||
vars: {
|
||||
WORKER_NAME: workerId,
|
||||
TLDRAW_ENV: env.TLDRAW_ENV,
|
||||
SENTRY_DSN: env.SENTRY_BEMO_WORKER_DSN,
|
||||
},
|
||||
sentry: {
|
||||
authToken: env.SENTRY_AUTH_TOKEN,
|
||||
project: 'bemo-worker',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
main().catch(async (err) => {
|
||||
// don't notify discord on preview builds
|
||||
if (env.TLDRAW_ENV !== 'preview') {
|
||||
await discord.message(`${Discord.AT_TEAM_MENTION} Deploy failed: ${err.stack}`, {
|
||||
always: true,
|
||||
})
|
||||
}
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
|
@ -1,12 +1,18 @@
|
|||
import * as github from '@actions/github'
|
||||
import { GetObjectCommand, ListObjectsV2Command, S3Client } from '@aws-sdk/client-s3'
|
||||
import { Upload } from '@aws-sdk/lib-storage'
|
||||
import assert from 'assert'
|
||||
import { execSync } from 'child_process'
|
||||
import { appendFileSync, existsSync, readdirSync, writeFileSync } from 'fs'
|
||||
import path, { join } from 'path'
|
||||
import { existsSync, readdirSync, writeFileSync } from 'fs'
|
||||
import path from 'path'
|
||||
import { PassThrough } from 'stream'
|
||||
import * as tar from 'tar'
|
||||
import {
|
||||
createGithubDeployment,
|
||||
getDeployInfo,
|
||||
setWranglerPreviewConfig,
|
||||
wranglerDeploy,
|
||||
} from './lib/deploy'
|
||||
import { Discord } from './lib/discord'
|
||||
import { exec } from './lib/exec'
|
||||
import { makeEnv } from './lib/makeEnv'
|
||||
import { nicelog } from './lib/nicelog'
|
||||
|
@ -48,26 +54,13 @@ const env = makeEnv([
|
|||
'R2_ACCESS_KEY_SECRET',
|
||||
])
|
||||
|
||||
const githubPrNumber = process.env.GITHUB_REF?.match(/refs\/pull\/(\d+)\/merge/)?.[1]
|
||||
function getPreviewId() {
|
||||
if (env.TLDRAW_ENV !== 'preview') return undefined
|
||||
if (githubPrNumber) return `pr-${githubPrNumber}`
|
||||
return process.env.TLDRAW_PREVIEW_ID ?? undefined
|
||||
}
|
||||
const previewId = getPreviewId()
|
||||
|
||||
if (env.TLDRAW_ENV === 'preview' && !previewId) {
|
||||
throw new Error(
|
||||
'If running preview deploys from outside of a PR action, TLDRAW_PREVIEW_ID env var must be set'
|
||||
)
|
||||
}
|
||||
const sha =
|
||||
// if the event is 'pull_request', github.context.sha is an ephemeral merge commit
|
||||
// while the actual commit we want to create the deployment for is the 'head' of the PR.
|
||||
github.context.eventName === 'pull_request'
|
||||
? github.context.payload.pull_request?.head.sha
|
||||
: github.context.sha
|
||||
const discord = new Discord({
|
||||
webhookUrl: env.DISCORD_DEPLOY_WEBHOOK_URL,
|
||||
shouldNotify: env.TLDRAW_ENV === 'production',
|
||||
totalSteps: 8,
|
||||
})
|
||||
|
||||
const { previewId, sha } = getDeployInfo()
|
||||
const sentryReleaseName = `${env.TLDRAW_ENV}-${previewId ? previewId + '-' : ''}-${sha}`
|
||||
|
||||
async function main() {
|
||||
|
@ -76,9 +69,9 @@ async function main() {
|
|||
'TLDRAW_ENV must be staging or production or preview'
|
||||
)
|
||||
|
||||
await discordMessage(`--- **${env.TLDRAW_ENV} deploy pre-flight** ---`)
|
||||
await discord.message(`--- **${env.TLDRAW_ENV} dotcom deploy pre-flight** ---`)
|
||||
|
||||
await discordStep('[1/7] setting up deploy', async () => {
|
||||
await discord.step('setting up deploy', async () => {
|
||||
// make sure the tldraw .css files are built:
|
||||
await exec('yarn', ['lazy', 'prebuild'])
|
||||
|
||||
|
@ -88,14 +81,14 @@ async function main() {
|
|||
|
||||
// deploy pre-flight steps:
|
||||
// 1. get the dotcom app ready to go (env vars and pre-build)
|
||||
await discordStep('[2/7] building dotcom app', async () => {
|
||||
await discord.step('building dotcom app', async () => {
|
||||
await createSentryRelease()
|
||||
await prepareDotcomApp()
|
||||
await uploadSourceMaps()
|
||||
await coalesceWithPreviousAssets(`${dotcom}/.vercel/output/static/assets`)
|
||||
})
|
||||
|
||||
await discordStep('[3/7] cloudflare deploy dry run', async () => {
|
||||
await discord.step('cloudflare deploy dry run', async () => {
|
||||
await deployAssetUploadWorker({ dryRun: true })
|
||||
await deployHealthWorker({ dryRun: true })
|
||||
await deployTlsyncWorker({ dryRun: true })
|
||||
|
@ -103,22 +96,22 @@ async function main() {
|
|||
|
||||
// --- point of no return! do the deploy for real --- //
|
||||
|
||||
await discordMessage(`--- **pre-flight complete, starting real deploy** ---`)
|
||||
await discord.message(`--- **pre-flight complete, starting real dotcom deploy** ---`)
|
||||
|
||||
// 2. deploy the cloudflare workers:
|
||||
await discordStep('[4/7] deploying asset uploader to cloudflare', async () => {
|
||||
await discord.step('deploying asset uploader to cloudflare', async () => {
|
||||
await deployAssetUploadWorker({ dryRun: false })
|
||||
})
|
||||
await discordStep('[5/7] deploying multiplayer worker to cloudflare', async () => {
|
||||
await discord.step('deploying multiplayer worker to cloudflare', async () => {
|
||||
await deployTlsyncWorker({ dryRun: false })
|
||||
})
|
||||
await discordStep('[6/7] deploying health worker to cloudflare', async () => {
|
||||
await discord.step('deploying health worker to cloudflare', async () => {
|
||||
await deployHealthWorker({ dryRun: false })
|
||||
})
|
||||
|
||||
// 3. deploy the pre-build dotcom app:
|
||||
const { deploymentUrl, inspectUrl } = await discordStep(
|
||||
'[7/7] deploying dotcom app to vercel',
|
||||
const { deploymentUrl, inspectUrl } = await discord.step(
|
||||
'deploying dotcom app to vercel',
|
||||
async () => {
|
||||
return await deploySpa()
|
||||
}
|
||||
|
@ -128,7 +121,7 @@ async function main() {
|
|||
|
||||
if (previewId) {
|
||||
const aliasDomain = `${previewId}-preview-deploy.tldraw.com`
|
||||
await discordStep('[8/7] aliasing preview deployment', async () => {
|
||||
await discord.step('aliasing preview deployment', async () => {
|
||||
await vercelCli('alias', ['set', deploymentUrl, aliasDomain])
|
||||
})
|
||||
|
||||
|
@ -136,9 +129,14 @@ async function main() {
|
|||
}
|
||||
|
||||
nicelog('Creating deployment for', deploymentUrl)
|
||||
await createGithubDeployment(deploymentAlias ?? deploymentUrl, inspectUrl)
|
||||
await createGithubDeployment(env, {
|
||||
app: 'dotcom',
|
||||
deploymentUrl: deploymentAlias ?? deploymentUrl,
|
||||
inspectUrl,
|
||||
sha,
|
||||
})
|
||||
|
||||
await discordMessage(`**Deploy complete!**`)
|
||||
await discord.message(`**Deploy complete!**`)
|
||||
}
|
||||
|
||||
async function prepareDotcomApp() {
|
||||
|
@ -167,21 +165,15 @@ async function prepareDotcomApp() {
|
|||
let didUpdateAssetUploadWorker = false
|
||||
async function deployAssetUploadWorker({ dryRun }: { dryRun: boolean }) {
|
||||
if (previewId && !didUpdateAssetUploadWorker) {
|
||||
appendFileSync(
|
||||
join(assetUpload, 'wrangler.toml'),
|
||||
`
|
||||
[env.preview]
|
||||
name = "${previewId}-tldraw-assets"`
|
||||
)
|
||||
await setWranglerPreviewConfig(assetUpload, { name: `${previewId}-tldraw-assets` })
|
||||
didUpdateAssetUploadWorker = true
|
||||
}
|
||||
await exec('yarn', ['wrangler', 'deploy', dryRun ? '--dry-run' : null, '--env', env.TLDRAW_ENV], {
|
||||
pwd: assetUpload,
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
// wrangler needs CI=1 set to prevent it from trying to do interactive prompts
|
||||
CI: '1',
|
||||
},
|
||||
|
||||
await wranglerDeploy({
|
||||
location: assetUpload,
|
||||
dryRun,
|
||||
env: env.TLDRAW_ENV,
|
||||
vars: {},
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -189,79 +181,39 @@ let didUpdateTlsyncWorker = false
|
|||
async function deployTlsyncWorker({ dryRun }: { dryRun: boolean }) {
|
||||
const workerId = `${previewId ?? env.TLDRAW_ENV}-tldraw-multiplayer`
|
||||
if (previewId && !didUpdateTlsyncWorker) {
|
||||
appendFileSync(
|
||||
join(worker, 'wrangler.toml'),
|
||||
`
|
||||
[env.preview]
|
||||
name = "${previewId}-tldraw-multiplayer"`
|
||||
)
|
||||
await setWranglerPreviewConfig(worker, { name: workerId })
|
||||
didUpdateTlsyncWorker = true
|
||||
}
|
||||
await exec(
|
||||
'yarn',
|
||||
[
|
||||
'wrangler',
|
||||
'deploy',
|
||||
dryRun ? '--dry-run' : null,
|
||||
'--env',
|
||||
env.TLDRAW_ENV,
|
||||
'--var',
|
||||
`SUPABASE_URL:${env.SUPABASE_LITE_URL}`,
|
||||
'--var',
|
||||
`SUPABASE_KEY:${env.SUPABASE_LITE_ANON_KEY}`,
|
||||
'--var',
|
||||
`SENTRY_DSN:${env.WORKER_SENTRY_DSN}`,
|
||||
'--var',
|
||||
`TLDRAW_ENV:${env.TLDRAW_ENV}`,
|
||||
'--var',
|
||||
`APP_ORIGIN:${env.APP_ORIGIN}`,
|
||||
'--var',
|
||||
`WORKER_NAME:${workerId}`,
|
||||
],
|
||||
{
|
||||
pwd: worker,
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
// wrangler needs CI=1 set to prevent it from trying to do interactive prompts
|
||||
CI: '1',
|
||||
},
|
||||
}
|
||||
)
|
||||
await wranglerDeploy({
|
||||
location: worker,
|
||||
dryRun,
|
||||
env: env.TLDRAW_ENV,
|
||||
vars: {
|
||||
SUPABASE_URL: env.SUPABASE_LITE_URL,
|
||||
SUPABASE_KEY: env.SUPABASE_LITE_ANON_KEY,
|
||||
SENTRY_DSN: env.WORKER_SENTRY_DSN,
|
||||
TLDRAW_ENV: env.TLDRAW_ENV,
|
||||
APP_ORIGIN: env.APP_ORIGIN,
|
||||
WORKER_NAME: workerId,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
let didUpdateHealthWorker = false
|
||||
async function deployHealthWorker({ dryRun }: { dryRun: boolean }) {
|
||||
if (previewId && !didUpdateHealthWorker) {
|
||||
appendFileSync(
|
||||
join(healthWorker, 'wrangler.toml'),
|
||||
`
|
||||
[env.preview]
|
||||
name = "${previewId}-tldraw-health"`
|
||||
)
|
||||
await setWranglerPreviewConfig(healthWorker, { name: `${previewId}-tldraw-health` })
|
||||
didUpdateHealthWorker = true
|
||||
}
|
||||
await exec(
|
||||
'yarn',
|
||||
[
|
||||
'wrangler',
|
||||
'deploy',
|
||||
dryRun ? '--dry-run' : null,
|
||||
'--env',
|
||||
env.TLDRAW_ENV,
|
||||
'--var',
|
||||
`DISCORD_HEALTH_WEBHOOK_URL:${env.DISCORD_HEALTH_WEBHOOK_URL}`,
|
||||
'--var',
|
||||
`HEALTH_WORKER_UPDOWN_WEBHOOK_PATH:${env.HEALTH_WORKER_UPDOWN_WEBHOOK_PATH}`,
|
||||
],
|
||||
{
|
||||
pwd: healthWorker,
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
// wrangler needs CI=1 set to prevent it from trying to do interactive prompts
|
||||
CI: '1',
|
||||
},
|
||||
}
|
||||
)
|
||||
await wranglerDeploy({
|
||||
location: healthWorker,
|
||||
dryRun,
|
||||
env: env.TLDRAW_ENV,
|
||||
vars: {
|
||||
DISCORD_HEALTH_WEBHOOK_URL: env.DISCORD_HEALTH_WEBHOOK_URL,
|
||||
HEALTH_WORKER_UPDOWN_WEBHOOK_PATH: env.HEALTH_WORKER_UPDOWN_WEBHOOK_PATH,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
type ExecOpts = NonNullable<Parameters<typeof exec>[2]>
|
||||
|
@ -293,61 +245,6 @@ async function vercelCli(command: string, args: string[], opts?: ExecOpts) {
|
|||
)
|
||||
}
|
||||
|
||||
function sanitizeVariables(errorOutput: string): string {
|
||||
const regex = /(--var\s+(\w+):[^ \n]+)/g
|
||||
|
||||
const sanitizedOutput = errorOutput.replace(regex, (_, match) => {
|
||||
const [variable] = match.split(':')
|
||||
return `${variable}:*`
|
||||
})
|
||||
|
||||
return sanitizedOutput
|
||||
}
|
||||
|
||||
async function discord(method: string, url: string, body: unknown): Promise<any> {
|
||||
const response = await fetch(`${env.DISCORD_DEPLOY_WEBHOOK_URL}${url}`, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error(`Discord webhook request failed: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
return response.json()
|
||||
}
|
||||
|
||||
const AT_TEAM_MENTION = '<@&959380625100513310>'
|
||||
async function discordMessage(content: string, { always = false }: { always?: boolean } = {}) {
|
||||
const shouldNotify = env.TLDRAW_ENV === 'production' || always
|
||||
if (!shouldNotify) {
|
||||
return {
|
||||
edit: () => {
|
||||
// noop
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const message = await discord('POST', '?wait=true', { content: sanitizeVariables(content) })
|
||||
|
||||
return {
|
||||
edit: async (newContent: string) => {
|
||||
await discord('PATCH', `/messages/${message.id}`, { content: sanitizeVariables(newContent) })
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async function discordStep<T>(content: string, cb: () => Promise<T>): Promise<T> {
|
||||
const message = await discordMessage(`${content}...`)
|
||||
try {
|
||||
const result = await cb()
|
||||
await message.edit(`${content} ✅`)
|
||||
return result
|
||||
} catch (err) {
|
||||
await message.edit(`${content} ❌`)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async function deploySpa(): Promise<{ deploymentUrl: string; inspectUrl: string }> {
|
||||
// both 'staging' and 'production' are deployed to vercel as 'production' deploys
|
||||
// in separate 'projects'
|
||||
|
@ -371,32 +268,6 @@ async function deploySpa(): Promise<{ deploymentUrl: string; inspectUrl: string
|
|||
return { deploymentUrl, inspectUrl }
|
||||
}
|
||||
|
||||
// Creates a github 'deployment', which creates a 'View Deployment' button in the PR timeline.
|
||||
async function createGithubDeployment(deploymentUrl: string, inspectUrl: string) {
|
||||
const client = github.getOctokit(env.GH_TOKEN)
|
||||
|
||||
const deployment = await client.rest.repos.createDeployment({
|
||||
owner: 'tldraw',
|
||||
repo: 'tldraw',
|
||||
ref: sha,
|
||||
payload: { web_url: deploymentUrl },
|
||||
environment: env.TLDRAW_ENV,
|
||||
transient_environment: true,
|
||||
required_contexts: [],
|
||||
auto_merge: false,
|
||||
task: 'deploy',
|
||||
})
|
||||
|
||||
await client.rest.repos.createDeploymentStatus({
|
||||
owner: 'tldraw',
|
||||
repo: 'tldraw',
|
||||
deployment_id: (deployment.data as any).id,
|
||||
state: 'success',
|
||||
environment_url: deploymentUrl,
|
||||
log_url: inspectUrl,
|
||||
})
|
||||
}
|
||||
|
||||
const sentryEnv = {
|
||||
SENTRY_AUTH_TOKEN: env.SENTRY_AUTH_TOKEN,
|
||||
SENTRY_ORG: 'tldraw',
|
||||
|
@ -526,7 +397,9 @@ async function coalesceWithPreviousAssets(assetsDir: string) {
|
|||
main().catch(async (err) => {
|
||||
// don't notify discord on preview builds
|
||||
if (env.TLDRAW_ENV !== 'preview') {
|
||||
await discordMessage(`${AT_TEAM_MENTION} Deploy failed: ${err.stack}`, { always: true })
|
||||
await discord.message(`${Discord.AT_TEAM_MENTION} Deploy failed: ${err.stack}`, {
|
||||
always: true,
|
||||
})
|
||||
}
|
||||
console.error(err)
|
||||
process.exit(1)
|
184
scripts/lib/deploy.ts
Normal file
184
scripts/lib/deploy.ts
Normal file
|
@ -0,0 +1,184 @@
|
|||
import * as github from '@actions/github'
|
||||
import { readFileSync } from 'fs'
|
||||
import { appendFile } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
import { env } from 'process'
|
||||
import toml from 'toml'
|
||||
import { exec } from './exec'
|
||||
|
||||
export function getDeployInfo() {
|
||||
const githubPrNumber = process.env.GITHUB_REF?.match(/refs\/pull\/(\d+)\/merge/)?.[1]
|
||||
|
||||
let previewId = process.env.TLDRAW_PREVIEW_ID
|
||||
if (!previewId && env.TLDRAW_ENV === 'preview') {
|
||||
if (githubPrNumber) {
|
||||
previewId = `pr-${githubPrNumber}`
|
||||
} else {
|
||||
throw new Error(
|
||||
'If running preview deploys from outside of a PR action, TLDRAW_PREVIEW_ID env var must be set'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const sha: string | undefined =
|
||||
// if the event is 'pull_request', github.context.sha is an ephemeral merge commit
|
||||
// while the actual commit we want to create the deployment for is the 'head' of the PR.
|
||||
github.context.eventName === 'pull_request'
|
||||
? github.context.payload.pull_request?.head.sha
|
||||
: github.context.sha
|
||||
|
||||
if (!sha) {
|
||||
throw new Error('Could not determine the SHA of the commit to deploy')
|
||||
}
|
||||
|
||||
return {
|
||||
githubPrNumber,
|
||||
previewId,
|
||||
sha,
|
||||
}
|
||||
}
|
||||
|
||||
// Creates a github 'deployment', which creates a 'View Deployment' button in the PR timeline.
|
||||
export async function createGithubDeployment(
|
||||
env: { GH_TOKEN: string; TLDRAW_ENV: string },
|
||||
{
|
||||
app,
|
||||
deploymentUrl,
|
||||
inspectUrl,
|
||||
sha,
|
||||
}: { app: string; deploymentUrl: string; inspectUrl?: string; sha: string }
|
||||
) {
|
||||
const client = github.getOctokit(env.GH_TOKEN)
|
||||
|
||||
const deployment = await client.rest.repos.createDeployment({
|
||||
owner: 'tldraw',
|
||||
repo: 'tldraw',
|
||||
ref: sha,
|
||||
payload: { web_url: deploymentUrl },
|
||||
environment: `${app}-${env.TLDRAW_ENV}`,
|
||||
transient_environment: env.TLDRAW_ENV === 'preview',
|
||||
production_environment: env.TLDRAW_ENV === 'production',
|
||||
required_contexts: [],
|
||||
auto_merge: false,
|
||||
task: 'deploy',
|
||||
})
|
||||
|
||||
await client.rest.repos.createDeploymentStatus({
|
||||
owner: 'tldraw',
|
||||
repo: 'tldraw',
|
||||
deployment_id: (deployment.data as any).id,
|
||||
state: 'success',
|
||||
environment_url: deploymentUrl,
|
||||
log_url: inspectUrl,
|
||||
})
|
||||
}
|
||||
|
||||
/** Deploy a worker to wrangler, returning the deploy ID */
|
||||
export async function wranglerDeploy({
|
||||
location,
|
||||
dryRun,
|
||||
env,
|
||||
vars,
|
||||
sentry,
|
||||
}: {
|
||||
location: string
|
||||
dryRun: boolean
|
||||
env: string
|
||||
vars: Record<string, string>
|
||||
sentry?: {
|
||||
authToken: string
|
||||
project: string
|
||||
release?: string
|
||||
}
|
||||
}) {
|
||||
const varsArray = []
|
||||
for (const [key, value] of Object.entries(vars)) {
|
||||
varsArray.push('--var', `${key}:${value}`)
|
||||
}
|
||||
|
||||
const out = await exec(
|
||||
'yarn',
|
||||
[
|
||||
'wrangler',
|
||||
'deploy',
|
||||
dryRun ? '--dry-run' : null,
|
||||
'--env',
|
||||
env,
|
||||
'--outdir',
|
||||
'.wrangler/dist',
|
||||
...varsArray,
|
||||
],
|
||||
{
|
||||
pwd: location,
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
// wrangler needs CI=1 set to prevent it from trying to do interactive prompts
|
||||
CI: '1',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (dryRun) return
|
||||
|
||||
const versionMatch = out.match(/Current Version ID: (.+)/)
|
||||
if (!versionMatch) {
|
||||
throw new Error('Could not find the deploy ID in wrangler output')
|
||||
}
|
||||
|
||||
const workerName = toml.parse(readFileSync(join(location, 'wrangler.toml')).toString())?.env?.[
|
||||
env
|
||||
]?.name
|
||||
|
||||
if (!workerName) {
|
||||
throw new Error('Could not find the worker name in wrangler output')
|
||||
}
|
||||
|
||||
if (sentry) {
|
||||
const release = sentry.release ?? `${workerName}.${versionMatch[1]}`
|
||||
|
||||
const sentryEnv = {
|
||||
SENTRY_AUTH_TOKEN: sentry.authToken,
|
||||
SENTRY_ORG: 'tldraw',
|
||||
SENTRY_PROJECT: sentry.project,
|
||||
}
|
||||
|
||||
// create a sentry release:
|
||||
exec('yarn', ['run', '-T', 'sentry-cli', 'releases', 'new', release], {
|
||||
pwd: location,
|
||||
env: sentryEnv,
|
||||
})
|
||||
|
||||
// upload sourcemaps to the release:
|
||||
exec(
|
||||
'yarn',
|
||||
[
|
||||
'run',
|
||||
'-T',
|
||||
'sentry-cli',
|
||||
'releases',
|
||||
'files',
|
||||
release,
|
||||
'upload-sourcemaps',
|
||||
'.wrangler/dist',
|
||||
],
|
||||
{
|
||||
pwd: location,
|
||||
env: sentryEnv,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function setWranglerPreviewConfig(
|
||||
location: string,
|
||||
{ name, customDomain }: { name: string; customDomain?: string }
|
||||
) {
|
||||
await appendFile(
|
||||
join(location, 'wrangler.toml'),
|
||||
`
|
||||
[env.preview]
|
||||
name = "${name}"
|
||||
${customDomain ? `routes = [ { pattern = "${customDomain}", custom_domain = true} ]` : ''}
|
||||
`
|
||||
)
|
||||
}
|
70
scripts/lib/discord.ts
Normal file
70
scripts/lib/discord.ts
Normal file
|
@ -0,0 +1,70 @@
|
|||
function sanitizeVariables(errorOutput: string): string {
|
||||
const regex = /(--var\s+(\w+):[^ \n]+)/g
|
||||
|
||||
const sanitizedOutput = errorOutput.replace(regex, (_, match) => {
|
||||
const [variable] = match.split(':')
|
||||
return `${variable}:*`
|
||||
})
|
||||
|
||||
return sanitizedOutput
|
||||
}
|
||||
|
||||
export class Discord {
|
||||
static AT_TEAM_MENTION = '<@&959380625100513310>'
|
||||
|
||||
constructor(opts: { webhookUrl: string; shouldNotify: boolean; totalSteps?: number }) {
|
||||
this.webhookUrl = opts.webhookUrl
|
||||
this.shouldNotify = opts.shouldNotify
|
||||
this.totalSteps = opts.totalSteps ?? 0
|
||||
}
|
||||
|
||||
webhookUrl: string
|
||||
shouldNotify: boolean
|
||||
totalSteps: number
|
||||
currentStep = 0
|
||||
|
||||
private async send(method: string, url: string, body: unknown): Promise<any> {
|
||||
const response = await fetch(`${this.webhookUrl}${url}`, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error(`Discord webhook request failed: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
return response.json()
|
||||
}
|
||||
|
||||
async message(content: string, { always = false }: { always?: boolean } = {}) {
|
||||
if (!always && !this.shouldNotify) {
|
||||
return {
|
||||
edit: () => {
|
||||
// noop
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const message = await this.send('POST', '?wait=true', { content: sanitizeVariables(content) })
|
||||
|
||||
return {
|
||||
edit: async (newContent: string) => {
|
||||
await this.send('PATCH', `/messages/${message.id}`, {
|
||||
content: sanitizeVariables(newContent),
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async step<T>(content: string, cb: () => Promise<T>): Promise<T> {
|
||||
this.currentStep++
|
||||
const message = await this.message(`[${this.currentStep}/${this.totalSteps}] ${content}...`)
|
||||
try {
|
||||
const result = await cb()
|
||||
await message.edit(`${content} ✅`)
|
||||
return result
|
||||
} catch (err) {
|
||||
await message.edit(`${content} ❌`)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
|
@ -35,7 +35,7 @@
|
|||
"@typescript-eslint/utils": "^5.59.0",
|
||||
"ast-types": "^0.14.2",
|
||||
"cross-fetch": "^3.1.5",
|
||||
"esbuild": "^0.18.4",
|
||||
"esbuild": "^0.21.5",
|
||||
"eslint": "^8.37.0",
|
||||
"glob": "^8.0.3",
|
||||
"gray-matter": "^4.0.3",
|
||||
|
@ -59,6 +59,7 @@
|
|||
"ignore": "^5.2.4",
|
||||
"minimist": "^1.2.8",
|
||||
"tar": "^7.0.1",
|
||||
"tmp": "^0.2.3"
|
||||
"tmp": "^0.2.3",
|
||||
"toml": "^3.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
import { ChildProcessWithoutNullStreams, spawn } from 'child_process'
|
||||
import kleur from 'kleur'
|
||||
import stripAnsi from 'strip-ansi'
|
||||
|
||||
// at the time of writing, workerd will regularly crash with a segfault
|
||||
// but the error is not caught by the process, so it will just hang
|
||||
// this script wraps the process, tailing the logs and restarting the process
|
||||
// if we encounter the string 'Segmentation fault'
|
||||
|
||||
import { ChildProcessWithoutNullStreams, spawn } from 'child_process'
|
||||
import stripAnsi from 'strip-ansi'
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
const log = console.log
|
||||
|
||||
class MiniflareMonitor {
|
||||
private process: ChildProcessWithoutNullStreams | null = null
|
||||
|
||||
|
@ -19,7 +16,7 @@ class MiniflareMonitor {
|
|||
|
||||
public start(): void {
|
||||
this.stop() // Ensure any existing process is stopped
|
||||
log(`Starting wrangler...`)
|
||||
console.log(`Starting wrangler...`)
|
||||
this.process = spawn(this.command, this.args, {
|
||||
env: {
|
||||
NODE_ENV: 'development',
|
||||
|
@ -42,12 +39,12 @@ class MiniflareMonitor {
|
|||
console.error('Segmentation fault detected. Restarting Miniflare...')
|
||||
this.restart()
|
||||
} else if (!err) {
|
||||
log(output.replace('[mf:inf]', '')) // or handle the output differently
|
||||
console.log(output.replace('[mf:inf]', '')) // or handle the output differently
|
||||
}
|
||||
}
|
||||
|
||||
private restart(): void {
|
||||
log('Restarting wrangler...')
|
||||
console.log('Restarting wrangler...')
|
||||
this.stop()
|
||||
setTimeout(() => this.start(), 3000) // Restart after a short delay
|
||||
}
|
||||
|
@ -60,7 +57,60 @@ class MiniflareMonitor {
|
|||
}
|
||||
}
|
||||
|
||||
const monitor = new MiniflareMonitor('wrangler', [
|
||||
class SizeReporter {
|
||||
lastLineTime = Date.now()
|
||||
nextTick?: NodeJS.Timeout
|
||||
|
||||
size = 0
|
||||
|
||||
start() {
|
||||
console.log('Spawning size reporter...')
|
||||
const proc = spawn('yarn', [
|
||||
'run',
|
||||
'-T',
|
||||
'esbuild',
|
||||
'src/worker.ts',
|
||||
'--bundle',
|
||||
'--minify',
|
||||
'--watch',
|
||||
'--external:cloudflare:*',
|
||||
'--target=esnext',
|
||||
'--format=esm',
|
||||
])
|
||||
// listen for lines on stdin
|
||||
proc.stdout.on('data', (data) => {
|
||||
this.size += data.length
|
||||
this.lastLineTime = Date.now()
|
||||
clearTimeout(this.nextTick)
|
||||
this.nextTick = setTimeout(() => {
|
||||
console.log(
|
||||
kleur.bold(kleur.yellow('worker')),
|
||||
'is roughly',
|
||||
kleur.bold(kleur.cyan(Math.floor(this.size / 1024) + 'kb')),
|
||||
'(minified)\n'
|
||||
)
|
||||
this.size = 0
|
||||
}, 10)
|
||||
})
|
||||
proc.stderr.on('data', (data) => {
|
||||
console.log(data.toString())
|
||||
})
|
||||
process.on('SIGINT', () => {
|
||||
console.log('Int')
|
||||
proc.kill()
|
||||
})
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('Term')
|
||||
proc.kill()
|
||||
})
|
||||
process.on('exit', () => {
|
||||
console.log('Exiting')
|
||||
proc.kill()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
new MiniflareMonitor('wrangler', [
|
||||
'dev',
|
||||
'--env',
|
||||
'dev',
|
||||
|
@ -69,5 +119,6 @@ const monitor = new MiniflareMonitor('wrangler', [
|
|||
'info',
|
||||
'--var',
|
||||
'IS_LOCAL:true',
|
||||
])
|
||||
monitor.start()
|
||||
]).start()
|
||||
|
||||
new SizeReporter().start()
|
Loading…
Reference in a new issue