Files
upstream-tldraw/internal/scripts/deploy-template.ts
upstream-mirror a774ac673a
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
2026-09-23 16:12:52 +05:00

101 lines
3.5 KiB
TypeScript

import { readdir } from 'fs/promises'
import { join } from 'path'
import { exec } from './lib/exec'
import { readJsonIfExists, REPO_ROOT } from './lib/file'
import { makeEnv } from './lib/makeEnv'
import { nicelog } from './lib/nicelog'
import { EXPORT_CONFIG_KEY, PackageJson } from './lib/types'
const TEMPLATE_DIR = join(REPO_ROOT, 'templates')
/**
* Like `makeEnv`, but also rejects empty values. Github expands an unconfigured secret or variable
* to the empty string rather than leaving it unset, so `makeEnv` alone would let a missing license
* key through and we'd deploy unlicensed demos on a green CI run — the exact thing this script
* exists to prevent.
*/
function requireEnv<const Keys extends readonly string[]>(keys: Keys) {
const env = makeEnv(keys)
const emptyVars = keys.filter((key) => env[key as keyof typeof env] === '')
if (emptyVars.length > 0) {
throw new Error(`Empty environment variables: ${emptyVars.join(', ')}`)
}
return env
}
// The license key for the demos deployed from here. Don't set one in the cloudflare dashboard as
// well: a key configured there is invisible to this repo, so nothing catches it silently expiring.
//
// The templates themselves never reference it. tldraw's LicenseProvider picks it up from the build
// environment (see getLicenseKeyFromEnv in packages/editor/src/lib/license/LicenseProvider.tsx), so
// the exported public templates stay free of our key.
const env = requireEnv(['TEMPLATES_TLDRAW_LICENSE_KEY'])
// every template deployed from here builds with vite. A next.js template would need
// NEXT_PUBLIC_TLDRAW_LICENSE_KEY instead.
const licenseEnv = { VITE_TLDRAW_LICENSE_KEY: env.TEMPLATES_TLDRAW_LICENSE_KEY }
async function main() {
const templateName = process.argv[2]
if (!templateName) {
nicelog('Usage: tsx deploy-template.ts <template-name>')
process.exit(1)
}
const possibleTemplates = (await readdir(TEMPLATE_DIR, { withFileTypes: true }))
.filter((d) => d.isDirectory())
.map((d) => d.name)
if (!possibleTemplates.includes(templateName)) {
nicelog(`Template "${templateName}" not found.`)
nicelog(`Possible templates: ${possibleTemplates.join(', ')}`)
process.exit(1)
}
const templateDir = join(TEMPLATE_DIR, templateName)
const packageJsonRaw = await readJsonIfExists(join(templateDir, 'package.json'))
if (!packageJsonRaw) {
nicelog('No package.json found')
process.exit(1)
}
const packageJson = PackageJson.validate(packageJsonRaw)
const deployConfig = packageJson[EXPORT_CONFIG_KEY]?.deploy
if (!deployConfig) {
nicelog(`Template "${templateName}" has no deploy config. Skipping.`)
process.exit(0)
}
await deployToCloudflare(templateName, templateDir)
nicelog(`Deployed ${templateName} to https://${deployConfig.host}`)
}
async function deployToCloudflare(templateName: string, templateDir: string) {
const cloudflareEnv = requireEnv(['CLOUDFLARE_ACCOUNT_ID', 'CLOUDFLARE_API_TOKEN'])
nicelog(`Building ${templateName}...`)
await exec('yarn', ['run', 'build'], {
pwd: templateDir,
env: { ...licenseEnv, NODE_ENV: 'production' },
})
// The route (including the custom domain) comes from the template's own wrangler.toml, which
// ships to users too — so the deploy target is described in one place rather than here.
nicelog(`Deploying ${templateName} to cloudflare...`)
await exec('yarn', ['wrangler', 'deploy'], {
pwd: templateDir,
env: {
...cloudflareEnv,
NODE_ENV: 'production',
// wrangler needs CI=1 set to prevent it from trying to do interactive prompts
CI: '1',
},
})
}
main().catch((err) => {
console.error(err)
process.exit(1)
})