4507ce6378
Bumps the npm_and_yarn group with 2 updates in the / directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) and [tar](https://github.com/isaacs/node-tar). Updates `vite` from 5.2.8 to 5.2.9 <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md">vite's changelog</a>.</em></p> <blockquote> <h2><!-- raw HTML omitted -->5.2.9 (2024-04-15)<!-- raw HTML omitted --></h2> <ul> <li>fix: <code>fsp.rm</code> removing files does not take effect (<a href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16032">#16032</a>) (<a href="https://github.com/vitejs/vite/commit/b05c405">b05c405</a>), closes <a href="https://redirect.github.com/vitejs/vite/issues/16032">#16032</a></li> <li>fix: fix accumulated stacks in error overlay (<a href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16393">#16393</a>) (<a href="https://github.com/vitejs/vite/commit/102c2fd">102c2fd</a>), closes <a href="https://redirect.github.com/vitejs/vite/issues/16393">#16393</a></li> <li>fix(deps): update all non-major dependencies (<a href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16376">#16376</a>) (<a href="https://github.com/vitejs/vite/commit/58a2938">58a2938</a>), closes <a href="https://redirect.github.com/vitejs/vite/issues/16376">#16376</a></li> <li>chore: update region comment (<a href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16380">#16380</a>) (<a href="https://github.com/vitejs/vite/commit/77562c3">77562c3</a>), closes <a href="https://redirect.github.com/vitejs/vite/issues/16380">#16380</a></li> <li>perf: reduce size of injected __vite__mapDeps code (<a href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16184">#16184</a>) (<a href="https://github.com/vitejs/vite/commit/c0ec6be">c0ec6be</a>), closes <a href="https://redirect.github.com/vitejs/vite/issues/16184">#16184</a></li> <li>perf(css): only replace empty chunk if imported (<a href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16349">#16349</a>) (<a href="https://github.com/vitejs/vite/commit/e2658ad">e2658ad</a>), closes <a href="https://redirect.github.com/vitejs/vite/issues/16349">#16349</a></li> </ul> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href="a77707d69c
"><code>a77707d</code></a> release: v5.2.9</li> <li><a href="102c2fd5ad
"><code>102c2fd</code></a> fix: fix accumulated stacks in error overlay (<a href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16393">#16393</a>)</li> <li><a href="58a2938a97
"><code>58a2938</code></a> fix(deps): update all non-major dependencies (<a href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16376">#16376</a>)</li> <li><a href="77562c3ff2
"><code>77562c3</code></a> chore: update region comment (<a href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16380">#16380</a>)</li> <li><a href="b05c405f68
"><code>b05c405</code></a> fix: <code>fsp.rm</code> removing files does not take effect (<a href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16032">#16032</a>)</li> <li><a href="e2658ad6fe
"><code>e2658ad</code></a> perf(css): only replace empty chunk if imported (<a href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16349">#16349</a>)</li> <li><a href="c0ec6bea69
"><code>c0ec6be</code></a> perf: reduce size of injected __vite__mapDeps code (<a href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16184">#16184</a>)</li> <li>See full diff in <a href="https://github.com/vitejs/vite/commits/v5.2.9/packages/vite">compare view</a></li> </ul> </details> <br /> Updates `tar` from 6.2.1 to 7.0.1 <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md">tar's changelog</a>.</em></p> <blockquote> <h1>Changelog</h1> <h2>7.0</h2> <ul> <li>Rewrite in TypeScript, provide ESM and CommonJS hybrid interface</li> <li>Add tree-shake friendly exports, like <code>import('tar/create')</code> and <code>import('tar/read-entry')</code> to get individual functions or classes.</li> <li>Add <code>chmod</code> option that defaults to false, and deprecate <code>noChmod</code>. That is, reverse the default option regarding explicitly setting file system modes to match tar entry settings.</li> <li>Add <code>processUmask</code> option to avoid having to call <code>process.umask()</code> when <code>chmod: true</code> (or <code>noChmod: false</code>) is set.</li> </ul> <h2>6.2</h2> <ul> <li>Add support for brotli compression</li> <li>Add <code>maxDepth</code> option to prevent extraction into excessively deep folders.</li> </ul> <h2>6.1</h2> <ul> <li>remove dead link to benchmarks (<a href="https://redirect.github.com/isaacs/node-tar/issues/313">#313</a>) (<a href="https://github.com/yetzt"><code>@yetzt</code></a>)</li> <li>add examples/explanation of using tar.t (<a href="https://github.com/isaacs"><code>@isaacs</code></a>)</li> <li>ensure close event is emited after stream has ended (<a href="https://github.com/webark"><code>@webark</code></a>)</li> <li>replace deprecated String.prototype.substr() (<a href="https://github.com/CommanderRoot"><code>@CommanderRoot</code></a>, <a href="https://github.com/lukekarrys"><code>@lukekarrys</code></a>)</li> </ul> <h2>6.0</h2> <ul> <li>Drop support for node 6 and 8</li> <li>fix symlinks and hardlinks on windows being packed with <code>\</code>-style path targets</li> </ul> <h2>5.0</h2> <ul> <li>Address unpack race conditions using path reservations</li> <li>Change large-numbers errors from TypeError to Error</li> <li>Add <code>TAR_*</code> error codes</li> <li>Raise <code>TAR_BAD_ARCHIVE</code> warning/error when there are no valid entries found in an archive</li> <li>do not treat ignored entries as an invalid archive</li> <li>drop support for node v4</li> <li>unpack: conditionally use a file mapping to write files on Windows</li> <li>Set more portable 'mode' value in portable mode</li> <li>Set <code>portable</code> gzip option in portable mode</li> </ul> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Commits</summary> <ul> <li><a href="d99fce38eb
"><code>d99fce3</code></a> 7.0.1</li> <li><a href="af043922c0
"><code>af04392</code></a> Do not apply linkpath,global from global pax header</li> <li><a href="b0fbdea463
"><code>b0fbdea</code></a> 7.0.0</li> <li><a href="957da7506c
"><code>957da75</code></a> remove old lib folder</li> <li><a href="9a260c2dba
"><code>9a260c2</code></a> test verifying <a href="https://redirect.github.com/isaacs/node-tar/issues/398">#398</a> is fixed</li> <li><a href="2d89a4edc3
"><code>2d89a4e</code></a> Properly handle long linkpath in PaxHeader</li> <li><a href="314ec7e642
"><code>314ec7e</code></a> list: close file even if no error thrown</li> <li><a href="b3afdbb264
"><code>b3afdbb</code></a> unpack test: use modern tap features</li> <li><a href="2330416081
"><code>2330416</code></a> test: code style, prefer () to _ for empty fns</li> <li><a href="ae9ce7ec2a
"><code>ae9ce7e</code></a> test: fix normalize-unicode coverage on linux</li> <li>Additional commits viewable in <a href="https://github.com/isaacs/node-tar/compare/v6.2.1...v7.0.1">compare view</a></li> </ul> </details> <br /> Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore <dependency name> major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore <dependency name> minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore <dependency name>` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore <dependency name>` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore <dependency name> <ignore condition>` will remove the ignore condition of the specified dependency and ignore conditions You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/tldraw/tldraw/network/alerts). </details> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Mime Čuvalo <mimecuvalo@gmail.com> Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
531 lines
16 KiB
TypeScript
531 lines
16 KiB
TypeScript
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 { PassThrough } from 'stream'
|
|
import tar from 'tar'
|
|
import { exec } from './lib/exec'
|
|
import { makeEnv } from './lib/makeEnv'
|
|
import { nicelog } from './lib/nicelog'
|
|
|
|
const worker = path.relative(process.cwd(), path.resolve(__dirname, '../apps/dotcom-worker'))
|
|
const healthWorker = path.relative(process.cwd(), path.resolve(__dirname, '../apps/health-worker'))
|
|
const assetUpload = path.relative(
|
|
process.cwd(),
|
|
path.resolve(__dirname, '../apps/dotcom-asset-upload')
|
|
)
|
|
const dotcom = path.relative(process.cwd(), path.resolve(__dirname, '../apps/dotcom'))
|
|
|
|
// 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([
|
|
'APP_ORIGIN',
|
|
'ASSET_UPLOAD',
|
|
'CLOUDFLARE_ACCOUNT_ID',
|
|
'CLOUDFLARE_API_TOKEN',
|
|
'DISCORD_DEPLOY_WEBHOOK_URL',
|
|
'DISCORD_HEALTH_WEBHOOK_URL',
|
|
'HEALTH_WORKER_UPDOWN_WEBHOOK_PATH',
|
|
'GC_MAPS_API_KEY',
|
|
'RELEASE_COMMIT_HASH',
|
|
'SENTRY_AUTH_TOKEN',
|
|
'SENTRY_DSN',
|
|
'SUPABASE_LITE_ANON_KEY',
|
|
'SUPABASE_LITE_URL',
|
|
'TLDRAW_ENV',
|
|
'VERCEL_PROJECT_ID',
|
|
'VERCEL_ORG_ID',
|
|
'VERCEL_TOKEN',
|
|
'WORKER_SENTRY_DSN',
|
|
'MULTIPLAYER_SERVER',
|
|
'GH_TOKEN',
|
|
'R2_ACCESS_KEY_ID',
|
|
'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 sentryReleaseName = `${env.TLDRAW_ENV}-${previewId ? previewId + '-' : ''}-${sha}`
|
|
|
|
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 discordMessage(`--- **${env.TLDRAW_ENV} deploy pre-flight** ---`)
|
|
|
|
await discordStep('[1/7] setting up deploy', async () => {
|
|
// make sure the tldraw .css files are built:
|
|
await exec('yarn', ['lazy', 'prebuild'])
|
|
|
|
// link to vercel and supabase projects:
|
|
await vercelCli('link', ['--project', env.VERCEL_PROJECT_ID])
|
|
})
|
|
|
|
// 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 createSentryRelease()
|
|
await prepareDotcomApp()
|
|
await uploadSourceMaps()
|
|
await coalesceWithPreviousAssets(`${dotcom}/.vercel/output/static/assets`)
|
|
})
|
|
|
|
await discordStep('[3/7] cloudflare deploy dry run', async () => {
|
|
await deployAssetUploadWorker({ dryRun: true })
|
|
await deployHealthWorker({ dryRun: true })
|
|
await deployTlsyncWorker({ dryRun: true })
|
|
})
|
|
|
|
// --- point of no return! do the deploy for real --- //
|
|
|
|
await discordMessage(`--- **pre-flight complete, starting real deploy** ---`)
|
|
|
|
// 2. deploy the cloudflare workers:
|
|
await discordStep('[4/7] deploying asset uploader to cloudflare', async () => {
|
|
await deployAssetUploadWorker({ dryRun: false })
|
|
})
|
|
await discordStep('[5/7] deploying multiplayer worker to cloudflare', async () => {
|
|
await deployTlsyncWorker({ dryRun: false })
|
|
})
|
|
await discordStep('[6/7] 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',
|
|
async () => {
|
|
return await deploySpa()
|
|
}
|
|
)
|
|
|
|
let deploymentAlias = null as null | string
|
|
|
|
if (previewId) {
|
|
const aliasDomain = `${previewId}-preview-deploy.tldraw.com`
|
|
await discordStep('[8/7] aliasing preview deployment', async () => {
|
|
await vercelCli('alias', ['set', deploymentUrl, aliasDomain])
|
|
})
|
|
|
|
deploymentAlias = `https://${aliasDomain}`
|
|
}
|
|
|
|
nicelog('Creating deployment for', deploymentUrl)
|
|
await createGithubDeployment(deploymentAlias ?? deploymentUrl, inspectUrl)
|
|
|
|
await discordMessage(`**Deploy complete!**`)
|
|
}
|
|
|
|
async function prepareDotcomApp() {
|
|
// pre-build the app:
|
|
await exec('yarn', ['build-app'], {
|
|
env: {
|
|
NEXT_PUBLIC_TLDRAW_RELEASE_INFO: `${env.RELEASE_COMMIT_HASH} ${new Date().toISOString()}`,
|
|
ASSET_UPLOAD: previewId
|
|
? `https://${previewId}-tldraw-assets.tldraw.workers.dev`
|
|
: env.ASSET_UPLOAD,
|
|
MULTIPLAYER_SERVER: previewId
|
|
? `https://${previewId}-tldraw-multiplayer.tldraw.workers.dev`
|
|
: env.MULTIPLAYER_SERVER,
|
|
NEXT_PUBLIC_CONTROL_SERVER: 'https://control.tldraw.com',
|
|
NEXT_PUBLIC_GC_API_KEY: env.GC_MAPS_API_KEY,
|
|
SENTRY_AUTH_TOKEN: env.SENTRY_AUTH_TOKEN,
|
|
SENTRY_ORG: 'tldraw',
|
|
SENTRY_PROJECT: 'lite',
|
|
SUPABASE_KEY: env.SUPABASE_LITE_ANON_KEY,
|
|
SUPABASE_URL: env.SUPABASE_LITE_URL,
|
|
TLDRAW_ENV: env.TLDRAW_ENV,
|
|
},
|
|
})
|
|
}
|
|
|
|
let didUpdateAssetUploadWorker = false
|
|
async function deployAssetUploadWorker({ dryRun }: { dryRun: boolean }) {
|
|
if (previewId && !didUpdateAssetUploadWorker) {
|
|
appendFileSync(
|
|
join(assetUpload, 'wrangler.toml'),
|
|
`
|
|
[env.preview]
|
|
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',
|
|
},
|
|
})
|
|
}
|
|
|
|
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"`
|
|
)
|
|
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',
|
|
},
|
|
}
|
|
)
|
|
}
|
|
|
|
let didUpdateHealthWorker = false
|
|
async function deployHealthWorker({ dryRun }: { dryRun: boolean }) {
|
|
if (previewId && !didUpdateHealthWorker) {
|
|
appendFileSync(
|
|
join(healthWorker, 'wrangler.toml'),
|
|
`
|
|
[env.preview]
|
|
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',
|
|
},
|
|
}
|
|
)
|
|
}
|
|
|
|
type ExecOpts = NonNullable<Parameters<typeof exec>[2]>
|
|
async function vercelCli(command: string, args: string[], opts?: ExecOpts) {
|
|
return exec(
|
|
'yarn',
|
|
[
|
|
'run',
|
|
'-T',
|
|
'vercel',
|
|
command,
|
|
'--token',
|
|
env.VERCEL_TOKEN,
|
|
'--scope',
|
|
env.VERCEL_ORG_ID,
|
|
'--yes',
|
|
...args,
|
|
],
|
|
{
|
|
...opts,
|
|
env: {
|
|
// specify org id via args instead of via env vars because otherwise it gets upset
|
|
// that there's no project id either
|
|
VERCEL_ORG_ID: env.VERCEL_ORG_ID,
|
|
VERCEL_PROJECT_ID: env.VERCEL_PROJECT_ID,
|
|
...opts?.env,
|
|
},
|
|
}
|
|
)
|
|
}
|
|
|
|
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'
|
|
const prod = env.TLDRAW_ENV !== 'preview'
|
|
const out = await vercelCli('deploy', ['--prebuilt', ...(prod ? ['--prod'] : [])], {
|
|
pwd: dotcom,
|
|
})
|
|
|
|
const previewURL = out.match(/Preview: (https:\/\/\S*)/)?.[1]
|
|
const inspectUrl = out.match(/Inspect: (https:\/\/\S*)/)?.[1]
|
|
const productionURL = out.match(/Production: (https:\/\/\S*)/)?.[1]
|
|
const deploymentUrl = previewURL ?? productionURL
|
|
|
|
if (!deploymentUrl) {
|
|
throw new Error('Could not find deployment URL in vercel output ' + out)
|
|
}
|
|
if (!inspectUrl) {
|
|
throw new Error('Could not find inspect URL in vercel output ' + out)
|
|
}
|
|
|
|
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',
|
|
SENTRY_PROJECT: 'lite',
|
|
}
|
|
|
|
const execSentry = (command: string, args: string[]) =>
|
|
exec(`yarn`, ['run', '-T', 'sentry-cli', command, ...args], { env: sentryEnv })
|
|
|
|
async function createSentryRelease() {
|
|
await execSentry('releases', ['new', sentryReleaseName])
|
|
if (!existsSync(`${dotcom}/sentry-release-name.ts`)) {
|
|
throw new Error('sentry-release-name.ts does not exist')
|
|
}
|
|
writeFileSync(
|
|
`${dotcom}/sentry-release-name.ts`,
|
|
`// This file is replaced during deployments to point to a meaningful release name in Sentry.` +
|
|
`// DO NOT MESS WITH THIS LINE OR THE ONE BELOW IT. I WILL FIND YOU\n` +
|
|
`export const sentryReleaseName = '${sentryReleaseName}'`
|
|
)
|
|
}
|
|
|
|
async function uploadSourceMaps() {
|
|
const sourceMapDir = `${dotcom}/dist/assets`
|
|
|
|
await execSentry('sourcemaps', ['upload', '--release', sentryReleaseName, sourceMapDir])
|
|
execSync('rm -rf ./.next/static/chunks/**/*.map')
|
|
}
|
|
|
|
const R2_URL = `https://c34edc4e76350954b63adebde86d5eb1.r2.cloudflarestorage.com`
|
|
const R2_BUCKET = `dotcom-deploy-assets-cache`
|
|
|
|
const R2 = new S3Client({
|
|
region: 'auto',
|
|
endpoint: R2_URL,
|
|
credentials: {
|
|
accessKeyId: env.R2_ACCESS_KEY_ID,
|
|
secretAccessKey: env.R2_ACCESS_KEY_SECRET,
|
|
},
|
|
})
|
|
|
|
/**
|
|
* When we run a vite prod build it creates a folder in the output dir called assets in which
|
|
* every file includes a hash of its contents in the filename. These files include files that
|
|
* are 'imported' by the js bundle, e.g. svg and css files, along with the js bundle itself
|
|
* (split into chunks).
|
|
*
|
|
* By default, when we deploy a new version of the app it will replace the previous versions
|
|
* of the files with new versions. This is problematic when we make a new deploy because if
|
|
* existing users have tldraw open in tabs while we make the new deploy, they might still try
|
|
* to fetch .js chunks or images which are no longer present on the server and may not have
|
|
* been cached by vercel's CDN in their location (or at all).
|
|
*
|
|
* To avoid this, we keep track of the assets from previous deploys in R2 and include them in the
|
|
* new deploy. This way, if a user has an old version of the app open in a tab, they will still
|
|
* be able to fetch the old assets from the previous deploy.
|
|
*/
|
|
async function coalesceWithPreviousAssets(assetsDir: string) {
|
|
nicelog('Saving assets to R2 bucket')
|
|
const objectKey = `${previewId ?? env.TLDRAW_ENV}/${new Date().toISOString()}+${sha}.tar.gz`
|
|
const pack = tar.c({ gzip: true, cwd: assetsDir }, readdirSync(assetsDir))
|
|
// Need to pipe through a PassThrough here because the tar stream is not a first class node stream
|
|
// and AWS's sdk expects a node stream (it checks `Body instanceof streams.Readable`)
|
|
const Body = new PassThrough()
|
|
pack.pipe(Body)
|
|
await new Upload({
|
|
client: R2,
|
|
params: {
|
|
Bucket: R2_BUCKET,
|
|
Key: objectKey,
|
|
Body,
|
|
},
|
|
}).done()
|
|
|
|
nicelog('Extracting previous assets from R2 bucket')
|
|
const { Contents } = await R2.send(
|
|
new ListObjectsV2Command({
|
|
Bucket: R2_BUCKET,
|
|
Prefix: `${previewId ?? env.TLDRAW_ENV}/`,
|
|
})
|
|
)
|
|
const [mostRecent, ...others] =
|
|
// filter out the one we just uploaded
|
|
Contents?.filter((obj) => obj.Key !== objectKey).sort(
|
|
(a, b) => (b.LastModified?.getTime() ?? 0) - (a.LastModified?.getTime() ?? 0)
|
|
) ?? []
|
|
|
|
if (!mostRecent) {
|
|
nicelog('No previous assets found')
|
|
return
|
|
}
|
|
|
|
// Always include the assets from the directly previous build, but also if there
|
|
// have been more deploys in the last two weeks, include those too.
|
|
const twoWeeks = 1000 * 60 * 60 * 24 * 14
|
|
const recentOthers = others.filter(
|
|
(o) => (o.LastModified?.getTime() ?? 0) > Date.now() - twoWeeks
|
|
)
|
|
const objectsToFetch = [mostRecent, ...recentOthers]
|
|
|
|
nicelog(
|
|
`Fetching ${objectsToFetch.length} previous assets from R2 bucket:`,
|
|
objectsToFetch.map((k) => k.Key)
|
|
)
|
|
for (const obj of objectsToFetch) {
|
|
const { Body } = await R2.send(
|
|
new GetObjectCommand({
|
|
Bucket: R2_BUCKET,
|
|
Key: obj.Key,
|
|
})
|
|
)
|
|
if (!Body) {
|
|
throw new Error(`Could not fetch object ${obj.Key}`)
|
|
}
|
|
// pipe into untar
|
|
// `keep-existing` is important here because we don't want to overwrite the new assets
|
|
// if they have the same name as the old assets becuase they will have different sentry debugIds
|
|
// and it will mess up the inline source viewer on sentry errors.
|
|
const out = tar.x({ cwd: assetsDir, 'keep-existing': true })
|
|
for await (const chunk of Body?.transformToWebStream() as any as AsyncIterable<Uint8Array>) {
|
|
out.write(Buffer.from(chunk.buffer))
|
|
}
|
|
out.end()
|
|
}
|
|
}
|
|
|
|
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 })
|
|
}
|
|
console.error(err)
|
|
process.exit(1)
|
|
})
|