snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
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

This commit is contained in:
upstream-mirror
2026-09-23 16:12:52 +05:00
commit a774ac673a
4541 changed files with 770499 additions and 0 deletions

73
.github/workflows/bump-versions.yml vendored Normal file
View File

@@ -0,0 +1,73 @@
name: Bump versions to latest tldraw version
# This bumps all package versions to match the latest tldraw version from npm
# Prevent more than one version bumping job from running at the same time
concurrency:
group: version-bump
# Only run on main branch
on:
workflow_dispatch:
defaults:
run:
shell: bash
permissions:
contents: read
jobs:
bump_versions:
name: 'Bump package versions to latest tldraw version'
environment: npm deploy
timeout-minutes: 30
runs-on: ubuntu-latest-16-cores-arm-open
steps:
- name: Generate GH token
id: generate_token
uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92
with:
app_id: ${{ secrets.HUPPY_APP_ID }}
private_key: ${{ secrets.HUPPY_APP_PRIVATE_KEY }}
- name: Check out code
id: checkout
uses: actions/checkout@v6
with:
persist-credentials: false
token: ${{ steps.generate_token.outputs.token }}
- name: Configure git credentials
run: git config --global url."https://x-access-token:${{ steps.generate_token.outputs.token }}@github.com/".insteadOf "https://github.com/"
- name: Check branch
run: |
if [[ "$(git rev-parse --abbrev-ref HEAD)" != "main" ]]; then
echo "ERROR This workflow only runs on the main branch."
exit 1
fi
- name: Run our setup
uses: ./.github/actions/setup
- name: Bump versions
run: |
git config --global user.name 'huppy-bot[bot]'
git config --global user.email '128400622+huppy-bot[bot]@users.noreply.github.com'
yarn tsx ./internal/scripts/bump-versions.ts
env:
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
# Preserve the setup action for post-job cleanup when checkout already succeeded.
- name: Check out Discord notify action
if: failure() && steps.checkout.outcome != 'success'
uses: actions/checkout@v6
with:
sparse-checkout: .github/actions/discord-fail-notify
- name: Notify Discord on failure
if: failure()
uses: ./.github/actions/discord-fail-notify
with:
webhook-url: ${{ secrets.DISCORD_DEPLOY_WEBHOOK_URL }}

112
.github/workflows/checks.yml vendored Normal file
View File

@@ -0,0 +1,112 @@
name: Checks
on:
pull_request:
merge_group:
push:
branches: [main]
env:
CI: 1
PRINT_GITHUB_ANNOTATIONS: 1
defaults:
run:
shell: bash
permissions:
contents: read
# update-pr-template reads the repo's label list (octo.issues.listLabelsForRepo).
issues: read
jobs:
test:
name: 'Tests & checks'
timeout-minutes: 15
runs-on: ubuntu-latest-16-cores-arm-open # TODO: this should probably run on multiple OSes
steps:
- name: Check out code
uses: actions/checkout@v6
- uses: ./.github/actions/setup
- name: Check for duplicate dependencies
run: yarn dedupe --check
- name: Check packages
run: yarn check-packages
- name: Check circular dependencies
run: yarn check-circular-deps
- name: Restore build tool caches
uses: actions/cache@v5
with:
path: |
.lazy
**/.lazy
**/.tsbuild
**/*.tsbuildinfo
key: test-tools-${{ runner.os }}-${{ hashFiles('yarn.lock', '.oxlintrc.json', '.oxfmtrc.json', 'lazy.config.ts') }}
- name: Typecheck
run: yarn build-types
- name: Check bundle sizes
run: yarn lazy check-bundle-size
- name: Check PR template
run: yarn update-pr-template --check
env:
GH_TOKEN: ${{ github.token }}
- name: Check API declarations and docs work as intended
run: yarn api-check
- name: Lint
run: yarn lint
- name: Check dotcom localizations
run: |
yarn build-i18n
git diff --exit-code --quiet ./apps/dotcom/client/public/tla/locales/ ./apps/dotcom/client/public/tla/locales-compiled/ || (echo "Please run yarn build-i18n and commit the changes" && exit 1)
# vitest's fsModuleCache (~54MB gzipped). Entries are content-hashed, so a stale
# restore is safe - unmatched modules are just re-transformed. Keyed on yarn.lock so
# one entry is reused across commits instead of saving a new copy on every run.
- name: Restore vitest module cache
uses: actions/cache@v5
with:
path: node_modules/.vitest-cache
key: vitest-cache-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: vitest-cache-${{ runner.os }}-
- name: Test
run: yarn test-ci
build:
name: 'Build all projects'
timeout-minutes: 15
runs-on: ubuntu-latest-16-cores-arm-open
steps:
- name: Check out code
uses: actions/checkout@v6
- uses: ./.github/actions/setup
- name: Restore build tool caches
uses: actions/cache@v5
with:
path: |
.lazy
**/.lazy
**/.tsbuild
**/*.tsbuildinfo
key: build-tools-${{ runner.os }}-${{ hashFiles('yarn.lock', 'lazy.config.ts') }}
- name: Build packages
# the sed pipe makes sure that github annotations come through without
# turbo's prefix
run: "yarn build-package | sed -E 's/^.*? ::/::/'"

52
.github/workflows/claude.yml vendored Normal file
View File

@@ -0,0 +1,52 @@
name: Claude Code
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]
permissions:
contents: read
jobs:
claude:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
# This is an optional setting that allows Claude to read CI results on PRs
additional_permissions: |
actions: read
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
# prompt: 'Update the pull request description to include a summary of changes.'
# Optional: Add claude_args to customize behavior and configuration
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://docs.anthropic.com/en/docs/claude-code/sdk#command-line for available options
# claude_args: '--model claude-opus-4-1-20250805 --allowed-tools Bash(gh pr:*)'

View File

@@ -0,0 +1,37 @@
name: Close inactive issues
on:
workflow_dispatch:
schedule:
- cron: '0 0 1 * *'
defaults:
run:
shell: bash
permissions:
contents: read
jobs:
close-issues:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v10
with:
days-before-issue-stale: 150
days-before-issue-close: 30
stale-issue-label: 'stale'
stale-issue-message: >
This issue has been marked as stale because it has been open for 150 days with no activity.
If there's no further discussion, it'll be closed in 30 days time.
If you would like to ensure it stays open, you can comment on the issue and revive the discussion.
Or, you can add the 'keep' label to prevent it from being closed.
close-issue-message: 'This issue was closed because it has been inactive for 30 days since being marked as stale.'
close-issue-reason: 'not_planned'
days-before-pr-stale: -1
days-before-pr-close: -1
operations-per-run: 1000
exempt-issue-labels: keep
repo-token: ${{ secrets.GITHUB_TOKEN }}

41
.github/workflows/dependabot-dedupe.yml vendored Normal file
View File

@@ -0,0 +1,41 @@
name: Dedupe Dependabot PRs
on:
push:
branches: ['dependabot/**']
permissions:
contents: write
pull-requests: write
repository-projects: write
defaults:
run:
shell: bash
jobs:
dedupe:
name: Dedupe Dependabot PRs
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v6
- uses: ./.github/actions/setup
- name: Configure Git
run: |
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
- name: Dedupe dependencies
run: yarn dedupe
- name: Commit changes
run: |
git add .
git commit -m 'Dedupe' || true
- name: Push changes
run: git push

55
.github/workflows/deploy-analytics.yml vendored Normal file
View File

@@ -0,0 +1,55 @@
name: Deploy analytics worker
permissions:
contents: read
deployments: write
on:
pull_request:
paths:
- 'apps/analytics-worker/**'
- 'internal/scripts/deploy-analytics.ts'
- 'internal/scripts/lib/deploy.ts'
- '.github/workflows/deploy-analytics.yml'
- '.github/actions/setup/**'
push:
branches:
- main
- production
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 analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }}
timeout-minutes: 10
runs-on: ubuntu-latest
environment: ${{ github.ref == 'refs/heads/production' && 'deploy-production' || 'deploy-staging' }}
concurrency: analytics-worker-${{ github.ref == 'refs/heads/production' && 'deploy-production' || github.ref }}
steps:
- name: Check out code
uses: actions/checkout@v6
with:
submodules: true
- uses: ./.github/actions/setup
- name: Build types
run: yarn build-types
- name: Deploy
run: yarn tsx internal/scripts/deploy-analytics.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 }}

61
.github/workflows/deploy-bemo.yml vendored Normal file
View File

@@ -0,0 +1,61 @@
name: Deploy bemo
# TODO: add some sort of production trigger
on:
pull_request:
push:
branches:
- main
- bemo-production
env:
CI: 1
PRINT_GITHUB_ANNOTATIONS: 1
TLDRAW_ENV: ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }}
defaults:
run:
shell: bash
permissions:
contents: read
# createGithubDeployment (internal/scripts/lib/deploy.ts) registers the deployment that
# renders the 'View Deployment' button on the PR.
deployments: write
jobs:
deploy:
name: Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }}
timeout-minutes: 15
runs-on: ubuntu-latest
environment: ${{ github.ref == 'refs/heads/bemo-production' && 'bemo-production' || 'bemo-canary' }}
concurrency: bemo-${{ github.ref == 'refs/heads/bemo-production' && 'bemo-production' || github.ref }}
steps:
# - name: Notify initial start
# uses: MineBartekSA/discord-webhook@v2
# if: github.ref == 'refs/heads/bemo-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@v6
with:
submodules: true
- uses: ./.github/actions/setup
- name: Build types
run: yarn build-types
- name: Deploy
run: yarn tsx internal/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 }}

215
.github/workflows/deploy-dotcom.yml vendored Normal file
View File

@@ -0,0 +1,215 @@
name: Deploy .com
on:
pull_request:
types: [opened, synchronize, labeled, unlabeled]
push:
branches:
- main
- production
permissions:
contents: read
# createGithubDeployment (internal/scripts/lib/deploy.ts) registers the deployment that
# renders the 'View Deployment' button on the PR.
deployments: write
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 dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }}
timeout-minutes: 15
runs-on: ubuntu-latest-16-cores-arm-open
environment: ${{ github.ref == 'refs/heads/production' && 'deploy-production' || 'deploy-staging' }}
concurrency: dotcom-${{ github.ref == 'refs/heads/production' && 'deploy-production' || github.ref }}
if: |
github.event_name == 'push' ||
(github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'dotcom-preview-please'))
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 }} dotcom deploy: ${{ github.event.head_commit.message }} by ${{ github.event.head_commit.author.name }}'
- name: Check out code
uses: actions/checkout@v6
env:
# Abort a stalled fetch so checkout's retry kicks in instead of hanging until the job timeout
GIT_HTTP_LOW_SPEED_LIMIT: 1000
GIT_HTTP_LOW_SPEED_TIME: 60
- uses: ./.github/actions/setup
- name: Determine zero deploy target
id: deploy_target
env:
GH_TOKEN: ${{ github.token }}
run: |
if [[ "${GITHUB_REF}" == "refs/heads/production" ]]; then
echo "DEPLOY_ZERO=flyio-multinode" >> $GITHUB_ENV
elif [[ "${GITHUB_REF}" == "refs/heads/main" ]]; then
echo "DEPLOY_ZERO=flyio-multinode" >> $GITHUB_ENV
else
echo "DEPLOY_ZERO=flyio" >> $GITHUB_ENV
fi
- name: Install Supabase CLI
if: github.event_name == 'pull_request'
uses: supabase/setup-cli@ab058987d8d6c725971f6cf9d0b5c98467e30bd1 # v1
with:
# Pinned: v2.112.0 rejects the Management API's own timestamps, which
# breaks `branches get` and `branches delete`. See supabase/cli#6115.
version: 2.111.0
- name: Delete Supabase branch
if: contains(github.event.pull_request.labels.*.name, 'reset-preview-db')
env:
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
run: |
supabase branches delete "pr-${{ github.event.number }}" \
--project-ref "${{ vars.SUPABASE_PREVIEW_PROJECT_ID }}" || echo "::warning::Failed to delete Supabase branch (may not exist yet)"
- name: Create Supabase preview branch
if: github.event_name == 'pull_request'
env:
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
run: |
BRANCH_NAME="pr-${{ github.event.number }}"
PROJECT_REF="${{ vars.SUPABASE_PREVIEW_PROJECT_ID }}"
# Create branch if it doesn't already exist.
if supabase branches get "$BRANCH_NAME" \
--project-ref "$PROJECT_REF" > /dev/null 2>&1; then
echo "Branch $BRANCH_NAME already exists."
else
supabase branches create "$BRANCH_NAME" --project-ref "$PROJECT_REF" > /dev/null
echo "Branch $BRANCH_NAME created."
fi
# Wait for branch to be ready, then extract connection strings.
# Write to a temp file to avoid secrets leaking into CI step output.
BRANCH_JSON=$(mktemp)
for i in $(seq 1 24); do
# While the branch is still provisioning, `branches get` may exit
# non-zero and/or write a non-JSON line to stdout (the newer CLI
# renders an error there). Tolerate both: `|| true` keeps `set -e`
# from aborting on a non-zero get, and the guarded jq below treats
# any non-JSON / missing field as "not ready yet" so the loop retries.
supabase branches get "$BRANCH_NAME" \
--project-ref "$PROJECT_REF" \
-o json > "$BRANCH_JSON" 2>/dev/null || true
POSTGRES_URL_TRANSACTION=$(jq -r '.POSTGRES_URL // empty' "$BRANCH_JSON" 2>/dev/null || true)
if [ -n "$POSTGRES_URL_TRANSACTION" ]; then
echo "Branch $BRANCH_NAME is ready."
break
fi
echo "Waiting for branch to be ready ($i/24)..."
sleep 5
done
POSTGRES_URL_NON_POOLING=$(jq -r '.POSTGRES_URL_NON_POOLING // empty' "$BRANCH_JSON" 2>/dev/null || true)
rm "$BRANCH_JSON"
if [ -z "$POSTGRES_URL_TRANSACTION" ]; then
echo "::error::Branch not ready after 2 minutes — POSTGRES_URL still empty"
exit 1
fi
if [ -z "$POSTGRES_URL_NON_POOLING" ] || [ "$POSTGRES_URL_NON_POOLING" = "null" ]; then
echo "::error::Failed to get POSTGRES_URL_NON_POOLING from branch"
exit 1
fi
# Rewrite transaction pooler (port 6543, IPv6-only) to session pooler
# (port 5432, IPv4-compatible) since GitHub Actions runners are IPv4-only.
POSTGRES_URL="${POSTGRES_URL_TRANSACTION/:6543/:5432}"
echo "::add-mask::$POSTGRES_URL"
echo "::add-mask::$POSTGRES_URL_TRANSACTION"
echo "::add-mask::$POSTGRES_URL_NON_POOLING"
echo "POSTGRES_URL=$POSTGRES_URL" >> "$GITHUB_ENV"
echo "POSTGRES_URL_NON_POOLING=$POSTGRES_URL_NON_POOLING" >> "$GITHUB_ENV"
- name: Build types
run: yarn build-types
- name: Setup Fly cli
if: env.DEPLOY_ZERO == 'flyio' || env.DEPLOY_ZERO == 'flyio-multinode'
uses: superfly/flyctl-actions/setup-flyctl@ed8efb33836e8b2096c7fd3ba1c8afe303ebbff1 # 1.6
- name: Deploy
run: yarn tsx internal/scripts/deploy-dotcom.ts
env:
RELEASE_COMMIT_HASH: ${{ github.sha }}
GH_TOKEN: ${{ github.token }}
APP_ORIGIN: ${{ vars.APP_ORIGIN }}
ASSET_UPLOAD: ${{ vars.ASSET_UPLOAD }}
IMAGE_WORKER: ${{ vars.IMAGE_WORKER }}
MULTIPLAYER_SERVER: ${{ vars.MULTIPLAYER_SERVER }}
USER_CONTENT_SENTRY_DSN: ${{ secrets.USER_CONTENT_SENTRY_DSN }}
USER_CONTENT_URL: ${{ vars.USER_CONTENT_URL }}
NEXT_PUBLIC_GOOGLE_CLOUD_PROJECT_NUMBER: ${{ vars.NEXT_PUBLIC_GOOGLE_CLOUD_PROJECT_NUMBER }}
SUPABASE_LITE_URL: ${{ vars.SUPABASE_LITE_URL }}
VERCEL_ORG_ID: ${{ vars.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ vars.VERCEL_DOTCOM_PROJECT_ID }}
ANALYTICS_API_URL: ${{ secrets.ANALYTICS_API_URL }}
ANALYTICS_API_TOKEN: ${{ secrets.ANALYTICS_API_TOKEN }}
ASSET_UPLOAD_SENTRY_DSN: ${{ secrets.ASSET_UPLOAD_SENTRY_DSN }}
BOTCOM_POSTGRES_CONNECTION_STRING: ${{ env.POSTGRES_URL_NON_POOLING || secrets.BOTCOM_POSTGRES_CONNECTION_STRING }}
BOTCOM_POSTGRES_POOLED_CONNECTION_STRING: ${{ env.POSTGRES_URL || secrets.BOTCOM_POSTGRES_POOLED_CONNECTION_STRING }}
CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
MCP_SCREENSHOT_TOKEN_SECRET: ${{ secrets.MCP_SCREENSHOT_TOKEN_SECRET }}
DISCORD_DEPLOY_WEBHOOK_URL: ${{ secrets.DISCORD_DEPLOY_WEBHOOK_URL }}
DISCORD_FEEDBACK_WEBHOOK_URL: ${{ secrets.DISCORD_FEEDBACK_WEBHOOK_URL }}
PLAIN_API_KEY: ${{ secrets.PLAIN_API_KEY }}
PLAIN_LABEL_TYPE_ID: ${{ secrets.PLAIN_LABEL_TYPE_ID }}
PLAIN_WORKSPACE_ID: ${{ secrets.PLAIN_WORKSPACE_ID }}
DISCORD_HEALTH_WEBHOOK_URL: ${{ secrets.DISCORD_HEALTH_WEBHOOK_URL }}
GC_MAPS_API_KEY: ${{ secrets.GC_MAPS_API_KEY }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
HEALTH_CHECK_BEARER_TOKEN: ${{ secrets.HEALTH_CHECK_BEARER_TOKEN }}
HEALTH_WORKER_UPDOWN_WEBHOOK_PATH: ${{ secrets.HEALTH_WORKER_UPDOWN_WEBHOOK_PATH }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_ACCESS_KEY_SECRET: ${{ secrets.R2_ACCESS_KEY_SECRET }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_CSP_REPORT_URI: ${{ secrets.SENTRY_CSP_REPORT_URI }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SUPABASE_LITE_ANON_KEY: ${{ secrets.SUPABASE_LITE_ANON_KEY }}
TLDRAW_LICENSE: ${{ secrets.TLDRAW_LICENSE }}
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VITE_CLERK_PUBLISHABLE_KEY: ${{ secrets.VITE_CLERK_PUBLISHABLE_KEY }}
VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }}
VITE_GA4_MEASUREMENT_ID: ${{ secrets.VITE_GA4_MEASUREMENT_ID }}
WORKER_SENTRY_DSN: ${{ secrets.WORKER_SENTRY_DSN }}
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
ZERO_ADMIN_PASSWORD: ${{ secrets.ZERO_ADMIN_PASSWORD }}
ZERO_R2_ENDPOINT: ${{ secrets.ZERO_R2_ENDPOINT }}
ZERO_R2_BUCKET_NAME: ${{ secrets.ZERO_R2_BUCKET_NAME }}
ZERO_R2_ACCESS_KEY_ID: ${{ secrets.ZERO_R2_ACCESS_KEY_ID }}
ZERO_R2_SECRET_ACCESS_KEY: ${{ secrets.ZERO_R2_SECRET_ACCESS_KEY }}
ZERO_OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.ZERO_OTEL_EXPORTER_OTLP_ENDPOINT }}
ZERO_OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.ZERO_OTEL_EXPORTER_OTLP_HEADERS }}
# The deploy script posts its own "Deploy failed" message, but a job timeout
# kills it without running that handler, so cover the cancelled case here.
- name: Notify Discord on cancelled deploy
if: cancelled() && github.event_name == 'push'
uses: ./.github/actions/discord-fail-notify
with:
webhook-url: ${{ secrets.DISCORD_DEPLOY_WEBHOOK_URL }}
status: was cancelled (job timeout?)

115
.github/workflows/deploy-grafana.yml vendored Normal file
View File

@@ -0,0 +1,115 @@
name: Deploy grafana resources
permissions:
contents: read
on:
pull_request:
paths:
- 'internal/grafana/**'
- '.github/workflows/deploy-grafana.yml'
push:
branches:
- main
paths:
- 'internal/grafana/**'
- '.github/workflows/deploy-grafana.yml'
defaults:
run:
shell: bash
env:
GCX_VERSION: 1.0.0
GCX_SHA256: c88c65958d19d83dc3a192a2a5037e08307a9c776019f24249b89f369c9c4d41
jobs:
deploy:
name: ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }}
# Validate and push both need the Grafana secrets, which fork PRs don't get.
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository
timeout-minutes: 10
runs-on: ubuntu-latest
concurrency: grafana-deploy-${{ github.event_name == 'push' && 'main' || github.ref }}
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Install gcx
run: |
curl -fsSL -o gcx.tar.gz "https://github.com/grafana/gcx/releases/download/v${GCX_VERSION}/gcx_${GCX_VERSION}_linux_amd64.tar.gz"
echo "${GCX_SHA256} gcx.tar.gz" | sha256sum -c -
tar xzf gcx.tar.gz gcx
sudo mv gcx /usr/local/bin/gcx
gcx --version
- name: Validate resources
run: gcx resources validate -p internal/grafana/resources
env:
GRAFANA_SERVER: ${{ secrets.GRAFANA_SERVER }}
GRAFANA_TOKEN: ${{ secrets.GRAFANA_TOKEN }}
- name: Dry run
if: github.event_name == 'pull_request'
run: gcx resources push -p internal/grafana/resources --dry-run --on-error abort
env:
GRAFANA_SERVER: ${{ secrets.GRAFANA_SERVER }}
GRAFANA_TOKEN: ${{ secrets.GRAFANA_TOKEN }}
# Everything except alert rules, folders first. Two orderings matter and neither is
# the alphabetical one this glob would otherwise give: folders have to exist before
# anything annotated into them, and alert rules go last (see the step below for why).
# A commit adding a folder and a dashboard inside it lands both in one push, so the
# folder cannot be assumed to already be there.
- name: Push resources
if: github.event_name == 'push'
run: |
shopt -s nullglob
for dir in internal/grafana/resources/*folders*/; do
gcx resources push -p "$dir" --on-error abort
done
for dir in internal/grafana/resources/*/; do
case "$dir" in *folders*|*alertrules*) continue ;; esac
gcx resources push -p "$dir" --on-error abort
done
env:
GRAFANA_SERVER: ${{ secrets.GRAFANA_SERVER }}
GRAFANA_TOKEN: ${{ secrets.GRAFANA_TOKEN }}
# Alert rules are the one resource type that can fail here having passed the PR
# dry run, because alerting has no server-side dry-run — the PR reports them as
# "skipped". A newly created rule carrying group labels is rejected outright
# ("cannot set group when creating a new rule"), so the first time any new rule
# lands, this step is where it surfaces. Kept last and separate so that failure
# costs the rule, not every dashboard in the tree. Still fails the job.
- name: Push alert rules
if: github.event_name == 'push'
run: |
shopt -s nullglob
for dir in internal/grafana/resources/*alertrules*/; do
gcx resources push -p "$dir" --on-error abort
done
env:
GRAFANA_SERVER: ${{ secrets.GRAFANA_SERVER }}
GRAFANA_TOKEN: ${{ secrets.GRAFANA_TOKEN }}
# Notification templates can't go through `gcx resources push` — gcx's discovery
# registry ignores the notifications.alerting.grafana.app API group entirely — so
# template files live in templates/ (not resources/) and go through the classic
# provisioning API. The PUT is idempotent and sets `api` provenance, which locks
# UI edits the same way provisioned resources are locked.
- name: Push notification templates
if: github.event_name == 'push'
run: |
shopt -s nullglob
for f in internal/grafana/templates/*.gotmpl; do
name=$(basename "$f" .gotmpl)
jq -Rs '{template: .}' "$f" | curl -fsS -X PUT \
"$GRAFANA_SERVER/api/v1/provisioning/templates/$name" \
-H "Authorization: Bearer $GRAFANA_TOKEN" \
-H "Content-Type: application/json" \
-d @- > /dev/null
done
env:
GRAFANA_SERVER: ${{ secrets.GRAFANA_SERVER }}
GRAFANA_TOKEN: ${{ secrets.GRAFANA_TOKEN }}

63
.github/workflows/deploy-mcp-app.yml vendored Normal file
View File

@@ -0,0 +1,63 @@
name: Deploy MCP app
on:
# Auto-deploy whenever production is updated, keeping the public MCP worker on
# the same commit and SDK build as production dotcom. Only production triggers a
# deploy: there is no staging worker, so a push to main must not overwrite the
# shared `tldraw-mcp-app` worker.
push:
branches:
- production
workflow_dispatch:
inputs:
target:
description: 'Target ref to deploy'
required: true
default: 'production'
env:
CI: 1
PRINT_GITHUB_ANNOTATIONS: 1
TLDRAW_ENV: production
TARGET: ${{ github.event.inputs.target || github.ref_name }}
defaults:
run:
shell: bash
permissions:
contents: read
jobs:
deploy:
name: Deploy MCP app to production
timeout-minutes: 10
runs-on: ubuntu-latest
environment: deploy-production
concurrency: mcp-app-deploy-production
steps:
- name: Check out code
uses: actions/checkout@v6
with:
ref: ${{ env.TARGET }}
submodules: true
fetch-depth: 0
- uses: ./.github/actions/setup
- name: Build types
run: yarn build-types
- name: Deploy
run: yarn workspace mcp-app deploy
env:
VITE_TLDRAW_LICENSE_KEY: ${{ secrets.MCP_APP_TLDRAW_LICENSE_KEY }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
- name: Notify Discord on failure
if: failure()
uses: ./.github/actions/discord-fail-notify
with:
webhook-url: ${{ secrets.DISCORD_DEPLOY_WEBHOOK_URL }}

57
.github/workflows/deploy-templates.yml vendored Normal file
View File

@@ -0,0 +1,57 @@
name: Deploy templates
# Deploys the cloudflare-hosted starter kit demos at *.templates.tldraw.dev, which are the live
# embeds on the docs pages. Two things should reach them: a change to a template (so edits go live
# without waiting for a release), and a release (so the demos pick up the new SDK — an SDK bump
# never touches templates/, so the path filter alone would never fire for one). This workflow
# covers the first; publish.yml covers the second with a job of its own, using the same
# .github/actions/deploy-templates steps.
#
# The vercel-hosted demos (chat, shader, workflow) deploy through vercel's own git integration and
# aren't handled here.
on:
push:
branches:
- main
paths:
- 'templates/**'
workflow_dispatch:
defaults:
run:
shell: bash
permissions:
contents: read
jobs:
deploy:
name: Deploy templates
# CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID are secrets on this environment rather than on
# the repository, so the job has to select it to see them at all.
environment: deploy-production
timeout-minutes: 60
runs-on: ubuntu-latest-16-cores-arm-open
# a release deploy and a template-change deploy can otherwise race each other onto the same
# custom domains, and the loser silently wins.
concurrency:
group: deploy-templates
cancel-in-progress: false
steps:
- name: Check out code
uses: actions/checkout@v6
with:
persist-credentials: false
- uses: ./.github/actions/deploy-templates
with:
license-key: ${{ secrets.TEMPLATES_TLDRAW_LICENSE_KEY }}
cloudflare-account-id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
cloudflare-api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
- name: Notify Discord on failure
if: failure()
uses: ./.github/actions/discord-fail-notify
with:
webhook-url: ${{ secrets.DISCORD_DEPLOY_WEBHOOK_URL }}

183
.github/workflows/e2e-device.yml vendored Normal file
View File

@@ -0,0 +1,183 @@
name: End to end tests (device)
# Run-on-request device-realistic e2e tests.
#
# These run real Mobile Safari (iOS Simulator) and real Chrome (Android emulator)
# through the OS input pipeline via Appium + WebdriverIO. Unlike the Playwright
# suite, this exercises the browser's own gesture recogniser and platform
# behaviour, so it can catch regressions that synthetic DOM events (CDP) cannot.
# It currently covers multi-touch gestures (pinch), but the harness is meant to
# grow to other device-specific behaviour (exports, clipboard, shortcuts, etc.)
# and other platforms.
#
# It is slower and a bit flaky by nature, so it is NOT part of the per-PR checks.
# Trigger it on demand:
#
# - Manually from the Actions tab (workflow_dispatch), choosing the platform.
# - By adding the `e2e-device-tests` label to a PR that touches device behaviour.
on:
workflow_dispatch:
inputs:
platform:
description: Which platform(s) to run
type: choice
default: both
options: [both, ios, android]
ios_device:
description: iOS simulator device name (optional; auto-picks newest iPhone if empty)
type: string
default: ''
pull_request:
types: [labeled]
env:
CI: 1
EXAMPLES_PORT: '5420'
defaults:
run:
shell: bash
# A new push to a PR (or a re-dispatch) cancels the previous in-flight run.
concurrency:
group: e2e-device-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
ios:
name: iOS Safari
# Run when dispatched with ios/both, or when a PR is labelled `e2e-device-tests`.
if: >
(github.event_name == 'workflow_dispatch' && inputs.platform != 'android') ||
(github.event_name == 'pull_request' && github.event.label.name == 'e2e-device-tests')
runs-on: macos-26 # pinned: image version determines Xcode + iOS sim runtimes
timeout-minutes: 40
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Run our setup
uses: ./.github/actions/setup
- name: Build examples
run: yarn run -T lazy refresh-assets && yarn run -T lazy run prebuild && cd apps/examples && yarn build
- name: Install Appium and the XCUITest driver
run: |
npm i -g appium@3
appium driver install xcuitest
- name: Resolve Xcode version (WDA cache key)
id: xcode
run: echo "version=$(xcodebuild -version | head -1 | awk '{print $2}')" >> "$GITHUB_OUTPUT"
- name: Cache WebDriverAgent build
# WDA is rebuilt from source on a cold simulator (~3.5 min). Cache its
# DerivedData keyed on the Xcode version so iterating on test fixes reuses
# the prebuilt WDA instead of rebuilding. Bump the trailing -vN to bust.
id: wda-cache
uses: actions/cache@v5
with:
path: ${{ runner.temp }}/wda-derived
key: wda-${{ runner.os }}-xcode-${{ steps.xcode.outputs.version }}-v1
- name: Boot iOS simulator
id: sim
# A fresh iOS simulator's first boot runs slow one-time migration plugins
# that exceed XCUITest's boot-wait. So boot ONE simulator here, wait for it
# to finish (`bootstatus -b`), and hand its UDID to Appium so it attaches to
# the ready device instead of booting its own. The available iPhone models
# depend on the Xcode the runner image ships, so auto-pick rather than
# hardcode a name (an explicit `ios_device` input wins if it matches).
env:
DEVICE_NAME: ${{ inputs.ios_device }}
run: |
xcrun simctl list devices available
SEL=$(xcrun simctl list devices available --json | node -e '
const data = JSON.parse(require("fs").readFileSync(0, "utf8"));
const want = process.env.DEVICE_NAME || "";
const runtimes = Object.keys(data.devices).filter(r => r.includes("iOS")).sort().reverse();
const phones = [];
for (const r of runtimes) for (const d of (data.devices[r] || [])) {
if (d.isAvailable !== false && d.name.startsWith("iPhone")) phones.push(d);
}
const pick = (want && phones.find(d => d.name === want)) || phones[0];
if (!pick) process.exit(2);
process.stdout.write(pick.udid + "|" + pick.name);
') || { echo "No available iPhone simulator on this runner"; exit 1; }
UDID="${SEL%%|*}"
NAME="${SEL#*|}"
echo "Using $NAME ($UDID)"
xcrun simctl boot "$UDID" || true
xcrun simctl bootstatus "$UDID" -b
echo "udid=$UDID" >> "$GITHUB_OUTPUT"
echo "name=$NAME" >> "$GITHUB_OUTPUT"
- name: Serve built examples
run: yarn preview --port "$EXAMPLES_PORT" &
working-directory: apps/examples
- name: Wait for examples server
run: npx --yes wait-on "http://localhost:$EXAMPLES_PORT/end-to-end" -t 120000
- name: Run device tests (iOS)
run: yarn e2e-device-ios
working-directory: apps/examples
env:
APPIUM_DEVICE_NAME: ${{ steps.sim.outputs.name }}
APPIUM_UDID: ${{ steps.sim.outputs.udid }}
# Build WDA into the cached dir; reuse the prebuilt one on a cache hit.
APPIUM_DERIVED_DATA_PATH: ${{ runner.temp }}/wda-derived
APPIUM_USE_PREBUILT_WDA: ${{ steps.wda-cache.outputs.cache-hit }}
android:
name: Android Chrome
if: >
(github.event_name == 'workflow_dispatch' && inputs.platform != 'ios') ||
(github.event_name == 'pull_request' && github.event.label.name == 'e2e-device-tests')
runs-on: ubuntu-latest
timeout-minutes: 40
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Run our setup
uses: ./.github/actions/setup
- name: Enable KVM (hardware acceleration for the emulator)
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \
| sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Build examples
run: yarn run -T lazy refresh-assets && yarn run -T lazy run prebuild && cd apps/examples && yarn build
- name: Install Appium and the UiAutomator2 driver
run: |
npm i -g appium@3
appium driver install uiautomator2
- name: Serve built examples
run: yarn preview --port "$EXAMPLES_PORT" &
working-directory: apps/examples
- name: Wait for examples server
run: npx --yes wait-on "http://localhost:$EXAMPLES_PORT/end-to-end" -t 120000
- name: Run device tests on emulator
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 34
arch: x86_64
target: google_apis
# The emulator's `localhost` is the emulator itself, so forward the host
# dev server into it before running the tests.
script: |
adb reverse tcp:${{ env.EXAMPLES_PORT }} tcp:${{ env.EXAMPLES_PORT }}
cd apps/examples && yarn e2e-device-android

35
.github/workflows/get-changelog.yml vendored Normal file
View File

@@ -0,0 +1,35 @@
name: Get draft changelog
on:
workflow_dispatch:
defaults:
run:
shell: bash
permissions:
contents: read
jobs:
deploy:
name: 'Get draft changlog'
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Run our setup
uses: ./.github/actions/setup
- name: Extract changelog
run: yarn extract-draft-changelog
- name: Upload changelog.md to artifacts
uses: actions/upload-artifact@v6
with:
name: changelog-output
path: changelog.md

View File

@@ -0,0 +1,73 @@
name: i18n download strings
on:
workflow_dispatch:
schedule:
- cron: '0 10 * * 1'
defaults:
run:
shell: bash
permissions:
contents: read
jobs:
download:
name: 'Download latest strings'
runs-on: ubuntu-latest
environment: 'deploy-production'
permissions:
# Give the default GITHUB_TOKEN write permission to commit and push the
# added or changed files to the repository.
contents: write
pull-requests: write
repository-projects: write
steps:
- name: Check out code
uses: actions/checkout@v6
with:
fetch-depth: 1
- uses: ./.github/actions/setup
- name: Download i18n strings
run: 'yarn i18n-download-strings'
env:
LOKALISE_API_TOKEN: ${{ secrets.LOKALISE_API_TOKEN }}
LOKALISE_PROJECT_ID: ${{ secrets.LOKALISE_PROJECT_ID }}
LOKALISE_TLDRAW_PROJECT_ID: ${{ secrets.LOKALISE_TLDRAW_PROJECT_ID }}
- name: Compile i18n strings
run: 'yarn build-i18n'
- name: Validate the strings
run: 'yarn i18n-check -f react-intl --locales ./apps/dotcom/client/public/tla/locales/ --source en --only invalidKeys'
- name: Commit and push changes
env:
GH_TOKEN: ${{ github.token }}
run: |
git config --global user.name 'huppy-bot[bot]'
git config --global user.email '128400622+huppy-bot[bot]@users.noreply.github.com'
# Check if there are any changes to JSON files
if ! git diff --quiet '*.json' || ! git diff --cached --quiet '*.json'; then
echo "Changes detected, creating PR..."
else
echo "No changes to commit"
exit 0
fi
# Delete the branch if it already exists
git push origin --delete update-i18n-strings 2>/dev/null || true
git checkout -b update-i18n-strings
git add "*.json"
git commit --no-verify -m '[automated] update i18n strings'
git push --set-upstream origin update-i18n-strings
gh pr create --title "[automated] update i18n strings" --body "This PR updates the i18n strings.
### Change type
- [x] \`other\`" --base main --head update-i18n-strings

View File

@@ -0,0 +1,35 @@
name: i18n upload strings
on:
workflow_dispatch:
push:
branches: [main]
defaults:
run:
shell: bash
permissions:
contents: read
jobs:
upload:
name: 'Upload latest strings'
runs-on: ubuntu-latest
environment: 'deploy-production'
steps:
- name: Check out code
uses: actions/checkout@v6
with:
fetch-depth: 1
- uses: ./.github/actions/setup
- name: Upload i18n strings
run: 'yarn i18n-upload-strings'
env:
LOKALISE_API_TOKEN: ${{ secrets.LOKALISE_API_TOKEN }}
LOKALISE_PROJECT_ID: ${{ secrets.LOKALISE_PROJECT_ID }}
LOKALISE_TLDRAW_PROJECT_ID: ${{ secrets.LOKALISE_TLDRAW_PROJECT_ID }}
LOKALISE_TEAM_ID: ${{ secrets.LOKALISE_TEAM_ID }}
LOKALISE_CARD_ID: ${{ secrets.LOKALISE_CARD_ID }}

122
.github/workflows/issue-triage.yml vendored Normal file
View File

@@ -0,0 +1,122 @@
name: Issue Triage
on:
issues:
types: [opened]
workflow_dispatch:
inputs:
issue_number:
description: 'Issue number to triage (for testing)'
required: true
type: number
permissions:
contents: read
jobs:
triage:
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
id-token: write
env:
ISSUE_NUMBER: ${{ github.event.issue.number || inputs.issue_number }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Get issue details
id: issue
env:
GH_TOKEN: ${{ github.token }}
run: |
gh issue view ${{ env.ISSUE_NUMBER }} --json title,body,labels,state > /tmp/issue.json
# Fetch issue type via API (not yet supported in gh CLI)
ISSUE_TYPE=$(gh api repos/${{ github.repository }}/issues/${{ env.ISSUE_NUMBER }} --jq '.type.name // "none"')
# Merge the type into the JSON
jq --arg type "$ISSUE_TYPE" '. + {issueType: $type}' /tmp/issue.json > /tmp/issue_with_type.json
echo "details<<EOF" >> $GITHUB_OUTPUT
cat /tmp/issue_with_type.json >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Triage Issue with Claude
uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
prompt: |
You are an issue triage bot for the tldraw repository. Tidy up issue #${{ env.ISSUE_NUMBER }}.
## Issue details
```json
${{ steps.issue.outputs.details }}
```
## Tasks
### 1. Improve the title (if needed)
- Make sure the title is clear and descriptive
- Use sentence case (capitalize only first word and proper nouns)
- For bugs: describe the symptom (e.g., "Arrow bindings break with rotated shapes")
- For features: use imperative mood (e.g., "Add padding option to zoomToFit")
- If the title is already good, leave it as-is
### 2. Improve the body (if needed)
- If the issue is written in a language other than English, add an English translation at the top of the body (keep the original text below)
- Make sure the body has enough context to understand the issue
- If the body is already good, leave it as-is
To update the title or body:
```bash
gh issue edit ${{ env.ISSUE_NUMBER }} --title "New title" --body "New body"
```
### 3. Set the issue type
Determine if this is a:
- **Bug**: Something is broken or not working as designed (crashes, rendering errors, data loss)
- **Feature**: A request for new functionality or enhancements (including UX preferences like "I wish X worked like Y")
- **Task**: Internal team task
- **Example**: Request for a new SDK example
Important: Distinguish between TRUE bugs and UX preferences disguised as bugs:
- TRUE BUG: "Clicking save crashes the app" or "Text doesn't render correctly"
- NOT A BUG (use Feature): "I don't like the color of the button" or "The menu should be on the left instead of right"
To set or change the issue type:
```bash
gh api repos/${{ github.repository }}/issues/${{ env.ISSUE_NUMBER }} -X PATCH -f type="Bug"
```
Replace "Bug" with the appropriate type (Bug, Feature, Task, or Example).
### 4. Apply appropriate labels
Use these labels as appropriate:
- `sdk` - Affects the tldraw SDK (@tldraw/tldraw package)
- `dotcom` - Related to tldraw.com
- `docs` - Changes only affect documentation
- `examples` - Request for a new SDK example
- `performance` - Performance-related issues
- `a11y` - Accessibility-related issues
- `bugfix` - For confirmed bug fixes
- `feature` - For new features
- `improvement` - For product improvements
- `More Info Needed` - If the issue lacks critical information needed to understand or reproduce
### 5. Assign a milestone (if appropriate)
If the issue clearly fits one of these milestones, assign it. Otherwise, leave the milestone empty.
Available milestones:
- **Improve developer resources**: For examples, documentation, improved code comments, starter kits, and `npm create tldraw` improvements
- **Improve automations**: For GitHub Actions, review bots, CI/CD, and other automation improvements
To assign a milestone:
```bash
gh issue edit ${{ env.ISSUE_NUMBER }} --milestone "Milestone Name"
```
Only assign a milestone if there's a clear fit. Most issues won't need a milestone.
## Output format
Use the GitHub CLI (`gh`) to make any changes. Do NOT add comments to the issue.
claude_args: '--allowedTools "Bash(gh issue:*),Bash(gh api:*)"'

52
.github/workflows/license-report.yml vendored Normal file
View File

@@ -0,0 +1,52 @@
name: Update license report
# Skipped on PRs from forks: the push below needs write access to the head branch.
permissions:
contents: write
on:
pull_request:
paths:
- '**/package.json'
- 'yarn.lock'
- 'internal/scripts/license-report.ts'
- '.github/workflows/license-report.yml'
env:
CI: 1
defaults:
run:
shell: bash
jobs:
update:
name: 'Update license report'
if: github.event.pull_request.head.repo.full_name == github.repository
timeout-minutes: 15
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
# `pull_request` checks out a detached merge commit, which we can't push back to.
ref: ${{ github.event.pull_request.head.ref }}
- uses: ./.github/actions/setup
- name: Configure Git
run: |
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
- name: Generate license report
run: yarn license-report --prod
- name: Commit changes
run: |
git add apps/docs/content/community/dependencies.mdx
git diff --cached --quiet || git commit -m 'Update license report'
git push

129
.github/workflows/playwright-dotcom.yml vendored Normal file
View File

@@ -0,0 +1,129 @@
name: End to end tests (dotcom)
on:
pull_request:
merge_group:
push:
branches: [main]
env:
CI: 1
PRINT_GITHUB_ANNOTATIONS: 1
TLDRAW_ENV: 'development'
# process-compose (the dev-app orchestrator) has no TTY in CI — run it headless.
PC_DISABLE_TUI: '1'
defaults:
run:
shell: bash
permissions:
contents: read
jobs:
check:
name: 'Check for relevant changes'
runs-on: ubuntu-latest
if: ${{ github.event.pull_request.head.repo.fork == false }}
outputs:
relevant: ${{ steps.includes.outputs.match == 'true' && steps.nondocs.outputs.match == 'true' }}
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Check for changes to relevant paths
uses: dorny/paths-filter@v3
id: includes
with:
filters: |
match:
- 'packages/**'
- 'apps/dotcom/**'
- '.github/workflows/playwright-dotcom.yml'
- '.github/actions/**'
- 'yarn.lock'
- name: Check for any non-doc changes
uses: dorny/paths-filter@v3
id: nondocs
with:
predicate-quantifier: 'every'
filters: |
match:
- '**'
- '!**/*.md'
- '!**/*.mdx'
build:
name: 'End to end tests (dotcom)'
needs: check
if: needs.check.outputs.relevant == 'true'
timeout-minutes: 20
runs-on: ubuntu-latest-8-cores-open
environment: 'e2e-dotcom'
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Run our setup
uses: ./.github/actions/setup
# No process-compose install step: `yarn dev-app` (the e2e webServer) calls the `process-compose`
# bin of @tldraw/scripts, which fetches the pinned binary on first use.
- name: Set up Playwright
uses: ./.github/actions/setup-playwright
- name: Build dotcom client
run: yarn run -T lazy refresh-assets && yarn run -T lazy run prebuild && yarn run -T lazy run build-i18n && cd apps/dotcom/client && yarn build
env:
VITE_CLERK_PUBLISHABLE_KEY: ${{ secrets.VITE_CLERK_PUBLISHABLE_KEY }}
- name: Run Playwright tests
run: |
echo "CLERK_PUBLISHABLE_KEY=${{ secrets.VITE_CLERK_PUBLISHABLE_KEY }}" > apps/dotcom/sync-worker/.dev.vars
echo "CLERK_SECRET_KEY=${{ secrets.CLERK_SECRET_KEY }}" >> apps/dotcom/sync-worker/.dev.vars
yarn e2e-dotcom
env:
VITE_CLERK_PUBLISHABLE_KEY: ${{ secrets.VITE_CLERK_PUBLISHABLE_KEY }}
CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }}
if: ${{!contains(github.event.pull_request.labels.*.name, 'e2e-x10') }}
- name: Run Playwright tests (x10)
run: |
echo "CLERK_PUBLISHABLE_KEY=${{ secrets.VITE_CLERK_PUBLISHABLE_KEY }}" > apps/dotcom/sync-worker/.dev.vars
echo "CLERK_SECRET_KEY=${{ secrets.CLERK_SECRET_KEY }}" >> apps/dotcom/sync-worker/.dev.vars
yarn e2e-dotcom-x10
env:
VITE_CLERK_PUBLISHABLE_KEY: ${{ secrets.VITE_CLERK_PUBLISHABLE_KEY }}
CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }}
if: contains(github.event.pull_request.labels.*.name, 'e2e-x10')
- uses: actions/upload-artifact@v6
if: always()
with:
name: playwright-report
path: apps/dotcom/client/playwright-report
retention-days: 30
- uses: shallwefootball/s3-upload-action@0d261a6f15b3b2e209dfebdecace4b100c04f95b # master
# only upload if we have AWS credentials:
env:
HAS_AWS_KEY: ${{ secrets.AWS_S3_SECRET_KEY && '1' || '' }}
if: always() && env.HAS_AWS_KEY == '1'
name: Upload S3
id: s3
with:
aws_key_id: ${{ secrets.AWS_S3_KEY_ID }}
aws_secret_access_key: ${{ secrets.AWS_S3_SECRET_KEY}}
aws_bucket: playwright.tldraw.xyz
source_dir: apps/dotcom/client/playwright-report
- name: Log report to summary
if: always()
run: |
report_url="https://playwright.tldraw.xyz/${{ steps.s3.outputs.object_key }}/index.html"
echo "Report: $report_url"
echo "## Playwright report" >> $GITHUB_STEP_SUMMARY
echo "* $report_url" >> $GITHUB_STEP_SUMMARY

View File

@@ -0,0 +1,106 @@
name: End to end tests (examples)
on:
pull_request:
merge_group:
push:
branches: [main]
env:
CI: 1
PRINT_GITHUB_ANNOTATIONS: 1
defaults:
run:
shell: bash
permissions:
contents: read
jobs:
check:
name: 'Check for relevant changes'
runs-on: ubuntu-latest
outputs:
relevant: ${{ steps.includes.outputs.match == 'true' && steps.nondocs.outputs.match == 'true' }}
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Check for changes to relevant paths
uses: dorny/paths-filter@v3
id: includes
with:
filters: |
match:
- 'packages/**'
- 'apps/examples/**'
- '.github/workflows/playwright-examples.yml'
- '.github/actions/**'
- 'yarn.lock'
- name: Check for any non-doc changes
uses: dorny/paths-filter@v3
id: nondocs
with:
predicate-quantifier: 'every'
filters: |
match:
- '**'
- '!**/*.md'
- '!**/*.mdx'
build:
name: 'End to end tests (examples)'
needs: check
if: needs.check.outputs.relevant == 'true'
timeout-minutes: 20
runs-on: ubuntu-latest-32-cores-open
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Run our setup
uses: ./.github/actions/setup
- name: Set up Playwright
uses: ./.github/actions/setup-playwright
- name: Build examples
run: yarn run -T lazy refresh-assets && yarn run -T lazy run prebuild && cd apps/examples && yarn build
- name: Run Playwright tests
run: 'yarn e2e'
working-directory: apps/examples
env:
GITHUB_REF_NAME: ${{ github.ref_name }}
GIT_COMMIT: ${{ github.sha }}
- uses: actions/upload-artifact@v6
if: always()
with:
name: playwright-report
path: apps/examples/playwright-report
retention-days: 30
- uses: shallwefootball/s3-upload-action@0d261a6f15b3b2e209dfebdecace4b100c04f95b # master
# only upload if we have AWS credentials:
env:
HAS_AWS_KEY: ${{ secrets.AWS_S3_SECRET_KEY && '1' || '' }}
if: always() && env.HAS_AWS_KEY == '1'
name: Upload S3
id: s3
with:
aws_key_id: ${{ secrets.AWS_S3_KEY_ID }}
aws_secret_access_key: ${{ secrets.AWS_S3_SECRET_KEY}}
aws_bucket: playwright.tldraw.xyz
source_dir: apps/examples/playwright-report
- name: Log report to summary
if: always()
run: |
report_url="https://playwright.tldraw.xyz/${{ steps.s3.outputs.object_key }}/index.html"
echo "Report: $report_url"
echo "## Playwright report" >> $GITHUB_STEP_SUMMARY
echo "* $report_url" >> $GITHUB_STEP_SUMMARY

View File

@@ -0,0 +1,71 @@
name: End to end tests (mermaid readability)
on:
pull_request:
merge_group:
push:
branches: [main]
env:
CI: 1
PRINT_GITHUB_ANNOTATIONS: 1
defaults:
run:
shell: bash
jobs:
check:
name: 'Check for relevant changes'
runs-on: ubuntu-latest
outputs:
relevant: ${{ steps.includes.outputs.match == 'true' }}
steps:
- name: Check out code
uses: actions/checkout@v6
# Only mermaid conversion changes can affect these checks, unlike the rest of the e2e suites.
- name: Check for changes to relevant paths
uses: dorny/paths-filter@v3
id: includes
with:
filters: |
match:
- 'packages/mermaid/**'
- 'apps/examples/src/examples/use-cases/hundred-mermaids/**'
- 'apps/examples/src/misc/mermaidReadability.ts'
- 'apps/examples/e2e/mermaid-readability/**'
- '.github/workflows/playwright-mermaid.yml'
- '.github/actions/**'
- 'yarn.lock'
build:
name: 'End to end tests (mermaid readability)'
needs: check
if: needs.check.outputs.relevant == 'true'
timeout-minutes: 20
runs-on: ubuntu-latest-16-cores-open
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Run our setup
uses: ./.github/actions/setup
- name: Set up Playwright
uses: ./.github/actions/setup-playwright
- name: Build examples
run: yarn run -T lazy refresh-assets && yarn run -T lazy run prebuild && cd apps/examples && yarn build
- name: Run Playwright tests (mermaid readability)
run: 'yarn e2e-mermaid-readability'
working-directory: apps/examples
- uses: actions/upload-artifact@v6
if: always()
with:
name: playwright-report
path: apps/examples/playwright-report
retention-days: 30

107
.github/workflows/playwright-perf.yml vendored Normal file
View File

@@ -0,0 +1,107 @@
name: End to end tests (perf)
on:
pull_request:
merge_group:
push:
branches: [main]
env:
CI: 1
PRINT_GITHUB_ANNOTATIONS: 1
defaults:
run:
shell: bash
permissions:
contents: read
jobs:
check:
name: 'Check for relevant changes'
runs-on: ubuntu-latest
outputs:
relevant: ${{ steps.includes.outputs.match == 'true' && steps.nondocs.outputs.match == 'true' }}
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Check for changes to relevant paths
uses: dorny/paths-filter@v3
id: includes
with:
filters: |
match:
- 'packages/**'
- 'apps/examples/**'
- '.github/workflows/playwright-perf.yml'
- '.github/actions/**'
- 'yarn.lock'
- name: Check for any non-doc changes
uses: dorny/paths-filter@v3
id: nondocs
with:
predicate-quantifier: 'every'
filters: |
match:
- '**'
- '!**/*.md'
- '!**/*.mdx'
build:
name: 'End to end tests (perf)'
needs: check
if: needs.check.outputs.relevant == 'true'
timeout-minutes: 20
runs-on: ubuntu-latest-16-cores-open
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Run our setup
uses: ./.github/actions/setup
- name: Set up Playwright
uses: ./.github/actions/setup-playwright
- name: Build examples
run: yarn run -T lazy refresh-assets && yarn run -T lazy run prebuild && cd apps/examples && yarn build
- name: Run Playwright tests (perf)
run: 'yarn e2e-perf'
working-directory: apps/examples
env:
PERFORMANCE_ANALYTICS_ENABLED: true
GITHUB_REF_NAME: ${{ github.ref_name }}
GIT_COMMIT: ${{ github.sha }}
- uses: actions/upload-artifact@v6
if: always()
with:
name: playwright-report
path: apps/examples/playwright-report
retention-days: 30
- uses: shallwefootball/s3-upload-action@0d261a6f15b3b2e209dfebdecace4b100c04f95b # master
# only upload if we have AWS credentials:
env:
HAS_AWS_KEY: ${{ secrets.AWS_S3_SECRET_KEY && '1' || '' }}
if: always() && env.HAS_AWS_KEY == '1'
name: Upload S3
id: s3
with:
aws_key_id: ${{ secrets.AWS_S3_KEY_ID }}
aws_secret_access_key: ${{ secrets.AWS_S3_SECRET_KEY}}
aws_bucket: playwright.tldraw.xyz
source_dir: apps/examples/playwright-report
- name: Log report to summary
if: always()
run: |
report_url="https://playwright.tldraw.xyz/${{ steps.s3.outputs.object_key }}/index.html"
echo "Report: $report_url"
echo "## Playwright report" >> $GITHUB_STEP_SUMMARY
echo "* $report_url" >> $GITHUB_STEP_SUMMARY

View File

@@ -0,0 +1,84 @@
# Adapted from https://mmazzarolo.com/blog/2022-09-09-visual-regression-testing-with-playwright-and-github-actions/
# This workflow's goal is forcing an update of the reference snapshots used
# by Playwright tests. Add the 'update-snapshots' label to a PR.
# From a high-level perspective, it works like this:
# 1. Because of a GitHub Action limitation, this workflow is triggered on every
# label added to a PR. We manually interrupt it unless the label is
# "update-snapshots".
# 2. Use the GitHub API to grab the information about the branch name and SHA of
# the latest commit of the current pull request.
# 3. Remove the label from the PR.
# 4. Update the Playwright reference snapshots based on the UI of this branch.
# 5. Commit the newly generated Playwright reference snapshots into this branch.
name: Update playwright snapshots
on:
pull_request:
types: [labeled]
env:
CI: 1
PRINT_GITHUB_ANNOTATIONS: 1
defaults:
run:
shell: bash
permissions:
contents: read
jobs:
update_snapshots:
name: 'Run'
timeout-minutes: 60
runs-on: ubuntu-latest-8-cores-open
if: github.event.label.name == 'update-snapshots'
permissions:
# Give the default GITHUB_TOKEN write permission to commit and push the
# added or changed files to the repository.
contents: write
pull-requests: write
steps:
- name: Remove the update-snapshots label
uses: actions-ecosystem/action-remove-labels@v1
with:
labels: update-snapshots
- name: Check out code
uses: actions/checkout@v6
with:
fetch-depth: 5
token: ${{ secrets.GITHUB_TOKEN }}
ref: ${{ github.event.pull_request.head.ref }}
- name: Run our setup
uses: ./.github/actions/setup
- name: Set up Playwright
uses: ./.github/actions/setup-playwright
- name: Build examples
run: yarn run -T lazy refresh-assets && yarn run -T lazy run prebuild && cd apps/examples && yarn build
- name: Run Playwright tests & update snapshots
run: 'yarn e2e --update-snapshots'
working-directory: 'apps/examples'
- name: Commit and push changes
if: always()
run: |
set -eux
git config --global user.name 'huppy-bot[bot]'
git config --global user.email '128400622+huppy-bot[bot]@users.noreply.github.com'
git add -A
if git diff --cached --quiet; then
echo 'No snapshot changes to commit.'
exit 0
fi
git commit --no-verify -m '[automated] update snapshots'
git add -A && git reset --hard HEAD # we sometimes get a dirty tree because husky makes changes for some reason
git pull --rebase
git push

56
.github/workflows/post-release-crew.yml vendored Normal file
View File

@@ -0,0 +1,56 @@
name: Post release crew to Discord
# Every Wednesday at 07:07 UTC, post to #development naming the people whose critical
# fixes / significant features are in this week's dotcom release.
# GitHub cron is UTC-only. We use a single fixed UTC time (no BST/GMT adjustment) so
# the job never fires twice: 07:07 UTC is 07:07 in London during winter (GMT) and
# 08:07 during summer (BST). GitHub often starts scheduled runs 15-60 minutes late,
# so this is scheduled early (and off the congested :00/:30 slots) to land before
# the working day ramps up. To change the day, edit the day-of-week field
# (`3` = Wednesday) in the cron line.
on:
schedule:
- cron: '7 7 * * 3' # Wednesday 07:07 UTC
workflow_dispatch: # allow manual runs
concurrency:
group: post-release-crew
permissions:
contents: read
defaults:
run:
shell: bash
jobs:
post-release-crew:
name: Post release crew to Discord
timeout-minutes: 20
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Fail fast if the Discord webhook is not configured
run: |
if [ -z "$DISCORD_RELEASE_WEBHOOK_URL" ]; then
echo "::error::DISCORD_RELEASE_WEBHOOK_URL secret is not set — the release crew message cannot be posted."
exit 1
fi
env:
DISCORD_RELEASE_WEBHOOK_URL: ${{ secrets.DISCORD_RELEASE_WEBHOOK_URL }}
- name: Install Claude Code
run: npm install -g @anthropic-ai/claude-code@2.1.206
- name: Run dotcom-release-crew skill
run: claude --dangerously-skip-permissions --print "/dotcom-release-crew"
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GH_TOKEN: ${{ github.token }}
DISCORD_RELEASE_WEBHOOK_URL: ${{ secrets.DISCORD_RELEASE_WEBHOOK_URL }}

View File

@@ -0,0 +1,57 @@
name: Post release marketing summary to Discord
# Every Wednesday at 07:07 UTC, post a plain-language summary of what's shipping in
# this week's dotcom release to the marketing team's Discord channel. Same schedule as
# post-release-crew.yml.
# GitHub cron is UTC-only. We use a single fixed UTC time (no BST/GMT adjustment) so
# the job never fires twice: 07:07 UTC is 07:07 in London during winter (GMT) and
# 08:07 during summer (BST). GitHub often starts scheduled runs 15-60 minutes late,
# so this is scheduled early (and off the congested :00/:30 slots) to land before
# the working day ramps up. To change the day, edit the day-of-week field
# (`3` = Wednesday) in the cron line.
on:
schedule:
- cron: '7 7 * * 3' # Wednesday 07:07 UTC
workflow_dispatch: # allow manual runs
concurrency:
group: post-release-marketing
permissions:
contents: read
defaults:
run:
shell: bash
jobs:
post-release-marketing:
name: Post release marketing summary to Discord
timeout-minutes: 20
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Fail fast if the Discord webhook is not configured
run: |
if [ -z "$DISCORD_MARKETING_WEBHOOK_URL" ]; then
echo "::error::DISCORD_MARKETING_WEBHOOK_URL secret is not set — the release marketing summary cannot be posted."
exit 1
fi
env:
DISCORD_MARKETING_WEBHOOK_URL: ${{ secrets.DISCORD_MARKETING_WEBHOOK_URL }}
- name: Install Claude Code
run: npm install -g @anthropic-ai/claude-code@2.1.206
- name: Run dotcom-release-marketing skill
run: claude --dangerously-skip-permissions --print "/dotcom-release-marketing"
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GH_TOKEN: ${{ github.token }}
DISCORD_MARKETING_WEBHOOK_URL: ${{ secrets.DISCORD_MARKETING_WEBHOOK_URL }}

View File

@@ -0,0 +1,64 @@
name: Prune preview deploys
on:
schedule:
# run once per day at midnight or whatever
- cron: '0 0 * * *'
workflow_dispatch:
permissions:
id-token: 'write'
env:
CI: 1
PRINT_GITHUB_ANNOTATIONS: 1
defaults:
run:
shell: bash
jobs:
deploy:
name: Prune preview deploys
timeout-minutes: 30
runs-on: ubuntu-latest
environment: deploy-staging
steps:
- name: Check out code
uses: actions/checkout@v6
with:
submodules: true
fetch-depth: 0
- uses: ./.github/actions/setup
- name: Setup Fly IO
uses: superfly/flyctl-actions/setup-flyctl@ed8efb33836e8b2096c7fd3ba1c8afe303ebbff1 # 1.6
- name: Configure AWS credentials using OIDC
uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: arn:aws:iam::571248596162:role/huppy-bot
aws-region: 'eu-north-1'
- name: Prune preview deploys
run: yarn tsx internal/scripts/prune-preview-deploys.ts
env:
GH_TOKEN: ${{ github.token }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
SUPABASE_PREVIEW_PROJECT_ID: ${{ vars.SUPABASE_PREVIEW_PROJECT_ID }}
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
ZERO_R2_ENDPOINT: ${{ secrets.ZERO_R2_ENDPOINT }}
ZERO_R2_BUCKET_NAME: ${{ secrets.ZERO_R2_BUCKET_NAME }}
ZERO_R2_ACCESS_KEY_ID: ${{ secrets.ZERO_R2_ACCESS_KEY_ID }}
ZERO_R2_SECRET_ACCESS_KEY: ${{ secrets.ZERO_R2_SECRET_ACCESS_KEY }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_ACCESS_KEY_SECRET: ${{ secrets.R2_ACCESS_KEY_SECRET }}
- name: Notify Discord on failure
if: failure()
uses: ./.github/actions/discord-fail-notify
with:
webhook-url: ${{ secrets.DISCORD_DEPLOY_WEBHOOK_URL }}

View File

@@ -0,0 +1,73 @@
name: Publish editor extensions
on:
push:
branches:
- main
- production
env:
CI: 1
PRINT_GITHUB_ANNOTATIONS: 1
defaults:
run:
shell: bash
permissions:
contents: read
jobs:
publish:
name: Publish editor extensions
environment: vsce publish
timeout-minutes: 20
runs-on: ubuntu-latest
permissions:
contents: write
concurrency: vscode-extension-publish
steps:
- name: Generate GH token
id: generate_token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.HUPPY_APP_ID }}
private-key: ${{ secrets.HUPPY_APP_PRIVATE_KEY }}
- name: Check out code
id: checkout
uses: actions/checkout@v6
with:
persist-credentials: false
submodules: true
token: ${{ steps.generate_token.outputs.token }}
- name: Configure git credentials
run: git config --global url."https://x-access-token:${{ steps.generate_token.outputs.token }}@github.com/".insteadOf "https://github.com/"
- uses: ./.github/actions/setup
- name: Build types
run: yarn build-types
- name: Publish extension
timeout-minutes: 10
run: yarn tsx ./internal/scripts/publish-editor-extensions.ts
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
OVSX_PAT: ${{ secrets.OVSX_PAT }}
TLDRAW_ENV: ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') }}
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
# Preserve the setup action for post-job cleanup when checkout already succeeded.
- name: Check out Discord notify action
if: failure() && steps.checkout.outcome != 'success'
uses: actions/checkout@v6
with:
sparse-checkout: .github/actions/discord-fail-notify
- name: Notify Discord on failure
if: failure()
uses: ./.github/actions/discord-fail-notify
with:
webhook-url: ${{ secrets.DISCORD_DEPLOY_WEBHOOK_URL }}

71
.github/workflows/publish-templates.yml vendored Normal file
View File

@@ -0,0 +1,71 @@
name: Publish templates
on:
workflow_dispatch:
workflow_call:
secrets:
HUPPY_PRIVATE_KEY:
required: true
HUPPY_APP_ID:
required: true
DISCORD_DEPLOY_WEBHOOK_URL:
required: false
env:
CI: 1
PRINT_GITHUB_ANNOTATIONS: 1
ALLOW_REFRESH_ASSETS_CHANGES: 1
defaults:
run:
shell: bash
permissions:
contents: read
jobs:
publish:
name: Publish templates
timeout-minutes: 60
runs-on: ubuntu-latest-16-cores-arm-open
steps:
- name: Check out code
uses: actions/checkout@v6
with:
persist-credentials: false
- uses: ./.github/actions/setup
- name: Generate a token
id: generate_token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.HUPPY_APP_ID }}
private-key: ${{ secrets.HUPPY_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
- name: Configure git
run: |
git config --global user.name 'huppy-bot[bot]'
git config --global user.email '128400622+huppy-bot[bot]@users.noreply.github.com'
- name: Export templates
env:
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
run: |
cd templates
all_templates=$(ls -d */ | cut -f1 -d'/')
cd ..
for template in $all_templates; do
echo "::group::Exporting $template"
yarn tsx ./internal/scripts/export-template.ts $template
echo "::endgroup::"
done
- name: Notify Discord on failure
if: failure()
uses: ./.github/actions/discord-fail-notify
with:
webhook-url: ${{ secrets.DISCORD_DEPLOY_WEBHOOK_URL }}

View File

@@ -0,0 +1,70 @@
name: Publish VSCE/OVX
on:
workflow_dispatch:
env:
CI: 1
PRINT_GITHUB_ANNOTATIONS: 1
defaults:
run:
shell: bash
permissions:
contents: read
jobs:
publish:
name: Publish VSCE/OVX
environment: vsce publish
timeout-minutes: 20
runs-on: ubuntu-latest
permissions:
contents: write
concurrency: vscode-extension-publish
steps:
- name: Generate GH token
id: generate_token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.HUPPY_APP_ID }}
private-key: ${{ secrets.HUPPY_APP_PRIVATE_KEY }}
- name: Check out code
id: checkout
uses: actions/checkout@v6
with:
persist-credentials: false
submodules: true
token: ${{ steps.generate_token.outputs.token }}
- name: Configure git credentials
run: git config --global url."https://x-access-token:${{ steps.generate_token.outputs.token }}@github.com/".insteadOf "https://github.com/"
- uses: ./.github/actions/setup
- name: Build types
run: yarn build-types
- name: Publish extension
timeout-minutes: 10
run: yarn tsx ./internal/scripts/publish-editor-extensions.ts
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
OVSX_PAT: ${{ secrets.OVSX_PAT }}
TLDRAW_ENV: production
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
# Preserve the setup action for post-job cleanup when checkout already succeeded.
- name: Check out Discord notify action
if: failure() && steps.checkout.outcome != 'success'
uses: actions/checkout@v6
with:
sparse-checkout: .github/actions/discord-fail-notify
- name: Notify Discord on failure
if: failure()
uses: ./.github/actions/discord-fail-notify
with:
webhook-url: ${{ secrets.DISCORD_DEPLOY_WEBHOOK_URL }}

330
.github/workflows/publish.yml vendored Normal file
View File

@@ -0,0 +1,330 @@
name: Publish SDK packages
on:
push:
branches:
- main
- production
- 'v*.*.x'
pull_request:
types: [labeled]
workflow_dispatch:
inputs:
publish_type:
type: choice
description: What to publish
required: true
default: manual
options:
- manual
- new
version:
description: Version to republish (manual only)
required: false
bump_type:
type: choice
description: Version bump type (new release only)
required: false
default: minor
options:
- minor
- major
- override
version_override:
type: string
description: Version override (new release only)
required: false
defaults:
run:
shell: bash
permissions:
contents: read
jobs:
remove-label:
name: Remove the publish-packages label
if: |
github.event_name == 'pull_request' &&
github.event.label.name == 'publish-packages' &&
github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Remove the publish-packages label
uses: actions-ecosystem/action-remove-labels@v1
with:
labels: publish-packages
publish:
name: Publish SDK packages
if: |
github.event_name != 'pull_request' ||
(
github.event.label.name == 'publish-packages' &&
github.event.pull_request.head.repo.full_name == github.repository
)
environment: npm deploy
timeout-minutes: 60
runs-on: ubuntu-latest-16-cores-arm-open
concurrency:
group: ${{ (github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/heads/v')) && 'npm-publish' || format('npm-publish-{0}', github.run_id) }}
permissions:
contents: read
id-token: write
outputs:
mode: ${{ steps.publish_mode.outputs.mode }}
version: ${{ steps.version.outputs.value }}
is_latest_version: ${{ steps.publish.outputs.is_latest_version }}
steps:
- name: Determine publish mode
id: publish_mode
env:
EVENT_NAME: ${{ github.event_name }}
EVENT_LABEL_NAME: ${{ github.event.label.name }}
REF: ${{ github.ref }}
REF_NAME: ${{ github.ref_name }}
DISPATCH_PUBLISH_TYPE: ${{ inputs.publish_type }}
DISPATCH_VERSION: ${{ inputs.version }}
DISPATCH_BUMP_TYPE: ${{ inputs.bump_type }}
DISPATCH_VERSION_OVERRIDE: ${{ inputs.version_override }}
run: |
mode=
bemo_url=https://demo.tldraw.xyz
use_huppy=false
case "$EVENT_NAME" in
push)
if [[ "$REF" == "refs/heads/main" ]]; then
mode=canary
bemo_url=https://canary-demo.tldraw.xyz
elif [[ "$REF" == "refs/heads/production" ]]; then
mode=next
bemo_url=https://canary-demo.tldraw.xyz
elif [[ "$REF_NAME" =~ ^v[0-9]+\.[0-9]+\.x$ ]]; then
mode=patch
use_huppy=true
fi
;;
pull_request)
if [[ "$EVENT_LABEL_NAME" == "publish-packages" ]]; then
mode=internal
bemo_url=https://canary-demo.tldraw.xyz
fi
;;
workflow_dispatch)
mode="$DISPATCH_PUBLISH_TYPE"
if [[ "$mode" == "manual" || "$mode" == "new" ]]; then
use_huppy=true
fi
;;
esac
if [[ -z "$mode" ]]; then
echo "ERROR Could not determine publish mode for $EVENT_NAME on $REF."
exit 1
fi
if [[ "$mode" == "manual" && -z "$DISPATCH_VERSION" ]]; then
echo "ERROR You must provide a version when manually republishing packages."
exit 1
fi
if [[ "$mode" == "new" ]]; then
if [[ "$REF_NAME" != "production" ]]; then
echo "ERROR You must be on the production branch to publish a new version."
exit 1
fi
if [[ "$DISPATCH_BUMP_TYPE" != "minor" && "$DISPATCH_BUMP_TYPE" != "major" && "$DISPATCH_BUMP_TYPE" != "override" ]]; then
echo "ERROR New releases must use a minor, major, or override bump type."
exit 1
fi
if [[ "$DISPATCH_BUMP_TYPE" != "override" && -n "$DISPATCH_VERSION_OVERRIDE" ]]; then
echo "ERROR You must set the bump type to override if you supply a custom version number override."
exit 1
fi
if [[ "$DISPATCH_BUMP_TYPE" == "override" && -z "$DISPATCH_VERSION_OVERRIDE" ]]; then
echo "ERROR You must supply a version override when using the override bump type."
exit 1
fi
fi
{
echo "mode=$mode"
echo "bemo_url=$bemo_url"
echo "use_huppy=$use_huppy"
} >> "$GITHUB_OUTPUT"
- name: Generate GH token
id: generate_token
if: steps.publish_mode.outputs.use_huppy == 'true'
uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92
with:
app_id: ${{ secrets.HUPPY_APP_ID }}
private_key: ${{ secrets.HUPPY_APP_PRIVATE_KEY }}
- name: Check out code
id: checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
persist-credentials: ${{ steps.publish_mode.outputs.use_huppy != 'true' }}
token: ${{ (steps.publish_mode.outputs.use_huppy == 'true' && steps.generate_token.outputs.token) || github.token }}
- name: Configure git credentials
if: steps.publish_mode.outputs.use_huppy == 'true'
run: |
git config --global url."https://x-access-token:${{ steps.generate_token.outputs.token }}@github.com/".insteadOf "https://github.com/"
git config --global user.name 'huppy-bot[bot]'
git config --global user.email '128400622+huppy-bot[bot]@users.noreply.github.com'
- name: Run our setup
uses: ./.github/actions/setup
- name: Publish
id: publish
env:
GH_TOKEN: ${{ (steps.publish_mode.outputs.use_huppy == 'true' && steps.generate_token.outputs.token) || github.token }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_ACCESS_KEY_SECRET: ${{ secrets.R2_ACCESS_KEY_SECRET }}
TLDRAW_BEMO_URL: ${{ steps.publish_mode.outputs.bemo_url }}
TLDRAW_VERSION_STRING: ${{ inputs.version }}
PUBLISH_MODE: ${{ steps.publish_mode.outputs.mode }}
BUMP_TYPE: ${{ inputs.bump_type }}
VERSION_OVERRIDE: ${{ inputs.version_override }}
run: |
case "$PUBLISH_MODE" in
canary | internal | next)
yarn tsx ./internal/scripts/publish-prerelease.ts "$PUBLISH_MODE"
;;
patch)
yarn tsx ./internal/scripts/publish-patch.ts
;;
manual)
yarn tsx ./internal/scripts/publish-manual.ts
;;
new)
bump="${BUMP_TYPE:-minor}"
if [[ "$bump" == "override" ]]; then
bump="$VERSION_OVERRIDE"
fi
yarn tsx ./internal/scripts/publish-new.ts --bump "$bump"
;;
*)
echo "ERROR Unknown publish mode: $PUBLISH_MODE"
exit 1
;;
esac
- name: Get published version
id: version
run: echo "value=$(jq -r .version ./packages/tldraw/package.json)" >> "$GITHUB_OUTPUT"
# Tell tldraw-internal to update its pinned SDK version. Fires only on a
# `next` publish — the prerelease channel the internal monorepo tracks.
# The tldraw-internal "Update tldraw" workflow listens for the
# `tldraw-release` repository_dispatch and resolves the latest `next`
# version itself, so the payload version is informational.
- name: Generate tldraw-internal token
if: steps.publish_mode.outputs.mode == 'next'
id: generate_internal_token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ vars.HUPPY_APP_ID }}
private-key: ${{ secrets.HUPPY_PRIVATE_KEY }}
owner: tldraw
repositories: tldraw-internal
- name: Notify tldraw-internal
if: steps.publish_mode.outputs.mode == 'next'
run: |
curl -X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ steps.generate_internal_token.outputs.token }}" \
https://api.github.com/repos/tldraw/tldraw-internal/dispatches \
-d '{"event_type":"tldraw-release","client_payload":{"version":"${{ steps.version.outputs.value }}","tag":"next"}}'
# Preserve the setup action for post-job cleanup when checkout already succeeded.
- name: Check out Discord notify action
if: failure() && steps.checkout.outcome != 'success'
uses: actions/checkout@v6
with:
sparse-checkout: .github/actions/discord-fail-notify
- name: Notify Discord on failure
if: failure()
uses: ./.github/actions/discord-fail-notify
with:
webhook-url: ${{ secrets.DISCORD_DEPLOY_WEBHOOK_URL }}
comment:
name: Tell user released version
if: needs.publish.outputs.mode == 'internal'
needs: publish
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Tell user released version
uses: actions-ecosystem/action-create-comment@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
body: |
Published version ${{ needs.publish.outputs.version }} to npm.
publish_templates:
name: Publishes code templates to separate repositories
uses: tldraw/tldraw/.github/workflows/publish-templates.yml@main
if: needs.publish.outputs.mode == 'new' || ((needs.publish.outputs.mode == 'patch' || needs.publish.outputs.mode == 'manual') && needs.publish.outputs.is_latest_version == 'true')
secrets:
HUPPY_PRIVATE_KEY: ${{ secrets.HUPPY_PRIVATE_KEY }}
HUPPY_APP_ID: ${{ vars.HUPPY_APP_ID }}
DISCORD_DEPLOY_WEBHOOK_URL: ${{ secrets.DISCORD_DEPLOY_WEBHOOK_URL }}
needs: publish
deploy_templates:
name: Deploys the template demos with the new SDK version
if: needs.publish.outputs.mode == 'new' || ((needs.publish.outputs.mode == 'patch' || needs.publish.outputs.mode == 'manual') && needs.publish.outputs.is_latest_version == 'true')
needs: publish
# CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID are secrets on this environment rather than on
# the repository, and a job has to select the environment to see them. Calling deploy-templates.yml
# as a reusable workflow doesn't do it: a called workflow only ever sees the secrets its caller
# hands it, so the v5.4.0 release reached the deploy with both empty (#10636) even though the
# called job selected deploy-production itself. The deploy runs here as a job of its own instead,
# sharing its steps with deploy-templates.yml through the composite action.
environment: deploy-production
timeout-minutes: 60
runs-on: ubuntu-latest-16-cores-arm-open
# same group as deploy-templates.yml: a release deploy and a template-change deploy can otherwise
# race each other onto the same custom domains, and the loser silently wins.
concurrency:
group: deploy-templates
cancel-in-progress: false
steps:
- name: Check out code
uses: actions/checkout@v6
with:
persist-credentials: false
- uses: ./.github/actions/deploy-templates
with:
license-key: ${{ secrets.TEMPLATES_TLDRAW_LICENSE_KEY }}
cloudflare-account-id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
cloudflare-api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
- name: Notify Discord on failure
if: failure()
uses: ./.github/actions/discord-fail-notify
with:
webhook-url: ${{ secrets.DISCORD_DEPLOY_WEBHOOK_URL }}

View File

@@ -0,0 +1,38 @@
name: Staging E2E Daily Cleanup
on:
schedule:
- cron: '45 1 * * *'
permissions:
contents: read
env:
CI: 1
defaults:
run:
shell: bash
jobs:
cleanup:
name: 'Daily Cleanup of Test Files'
timeout-minutes: 20
runs-on: ubuntu-latest
environment: 'e2e-dotcom'
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Run our setup
uses: ./.github/actions/setup
- name: Cleanup test files
run: yarn tsx internal/scripts/dotcom/cleanup-test-files.ts
env:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_ACCESS_KEY_SECRET: ${{ secrets.R2_ACCESS_KEY_SECRET }}
SUPABASE_STAGING_DB_URL: ${{ secrets.SUPABASE_STAGING_DB_URL }}
STAGING_OWNER_ID: ${{ secrets.STAGING_OWNER_ID }}

68
.github/workflows/staging-e2e.yml vendored Normal file
View File

@@ -0,0 +1,68 @@
name: Staging E2E Tests
on:
workflow_dispatch:
inputs:
test_files_count:
description: 'Number of test files to copy to staging'
required: false
default: '10'
type: number
permissions:
contents: read
env:
CI: 1
PRINT_GITHUB_ANNOTATIONS: 1
defaults:
run:
shell: bash
jobs:
test:
name: 'Staging E2E Tests'
timeout-minutes: 40
runs-on: ubuntu-latest-8-cores-open
environment: 'e2e-dotcom'
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Run our setup
uses: ./.github/actions/setup
- name: Set up Playwright
uses: ./.github/actions/setup-playwright
- name: Run cleanup of test files
run: yarn tsx internal/scripts/dotcom/cleanup-test-files.ts
env:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_ACCESS_KEY_SECRET: ${{ secrets.R2_ACCESS_KEY_SECRET }}
SUPABASE_STAGING_DB_URL: ${{ secrets.SUPABASE_STAGING_DB_URL }}
STAGING_OWNER_ID: ${{ secrets.STAGING_OWNER_ID }}
- name: Copy random files to staging
run: yarn tsx internal/scripts/dotcom/copy-random-files-to-staging.ts
env:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_ACCESS_KEY_SECRET: ${{ secrets.R2_ACCESS_KEY_SECRET }}
SUPABASE_PRODUCTION_DB_URL: ${{ secrets.SUPABASE_PRODUCTION_DB_URL }}
SUPABASE_STAGING_DB_URL: ${{ secrets.SUPABASE_STAGING_DB_URL }}
STAGING_OWNER_ID: ${{ secrets.STAGING_OWNER_ID }}
STAGING_TEST_FILES_COUNT: ${{ github.event.inputs.test_files_count || '10' }}
- name: Run staging e2e tests
run: yarn e2e-staging
working-directory: apps/dotcom/client
env:
CLERK_PUBLISHABLE_KEY: ${{ secrets.VITE_CLERK_PUBLISHABLE_KEY }}
CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }}
STAGING_TEST_EMAIL: ${{ secrets.STAGING_TEST_EMAIL }}
STAGING_TEST_PASSWORD: ${{ secrets.STAGING_TEST_PASSWORD }}
STAGING_TESTS: 'true'

View File

@@ -0,0 +1,69 @@
name: Trigger dotcom hotfix
# pull_request_target rather than push/pull_request, for the same reason as
# trigger-sdk-hotfix.yml: release-notes PRs squash-merge with `[skip ci]` in the title, which
# suppresses the push-to-main run entirely, and the `labeled` event fires before the merge so the
# `merged == true` gate rejects it. Neither path ever reached the script. GitHub's skip-ci markers
# do not apply to pull_request_target. `closed` replaces the old push trigger (which ran on every
# merge to main just to check labels), and `labeled` allows re-triggering by re-adding the label to
# an already-merged PR. Secrets exposure is safe here: the job only runs for merged PRs carrying a
# maintainer-applied hotfix label, checkout uses the trusted base branch, and the only PR-derived
# code the script builds is the PR's already-merged commit from main.
on:
pull_request_target:
types: [closed, labeled]
branches:
- main
defaults:
run:
shell: bash
permissions:
contents: write
pull-requests: write
jobs:
trigger:
name: Trigger a dotcom hotfix
runs-on: ubuntu-latest
timeout-minutes: 20
concurrency: dotcom-hotfix
environment: npm deploy
if: |
github.event.pull_request.merged == true &&
((github.event.action == 'closed' &&
contains(github.event.pull_request.labels.*.name, 'dotcom-hotfix-please')) ||
(github.event.action == 'labeled' &&
github.event.label.name == 'dotcom-hotfix-please'))
steps:
- name: Generate a token
id: generate_token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.HUPPY_APP_ID }}
private-key: ${{ secrets.HUPPY_APP_PRIVATE_KEY }}
- uses: actions/checkout@v6
with:
persist-credentials: false
token: ${{ steps.generate_token.outputs.token }}
fetch-depth: 0
- name: Configure git credentials
run: git config --global url."https://x-access-token:${{ steps.generate_token.outputs.token }}@github.com/".insteadOf "https://github.com/"
- uses: ./.github/actions/setup
- name: Maybe trigger dotcom hotfix
run: |
set -e
git config --global user.name 'huppy-bot[bot]'
git config --global user.email '128400622+huppy-bot[bot]@users.noreply.github.com'
git log --oneline -10
yarn tsx internal/scripts/trigger-dotcom-hotfix.ts
env:
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
DISCORD_DEPLOY_WEBHOOK_URL: ${{ secrets.DISCORD_DEPLOY_WEBHOOK_URL }}
PR_NUMBER: ${{ github.event.pull_request.number }}

View File

@@ -0,0 +1,118 @@
name: Trigger dotcom release
on:
push:
branches:
- hotfixes
workflow_dispatch:
inputs:
target:
description: 'Target ref to deploy'
required: true
default: 'main'
defaults:
run:
shell: bash
env:
TARGET: ${{ github.event.inputs.target }}
permissions:
contents: read
jobs:
trigger:
name: ${{ github.event_name == 'workflow_dispatch' && 'Manual trigger' || 'Hotfix' }}
runs-on: ubuntu-latest
concurrency: trigger-production
steps:
- name: Generate a token
id: generate_token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ vars.HUPPY_APP_ID }}
private-key: ${{ secrets.HUPPY_PRIVATE_KEY }}
- uses: actions/checkout@v6
with:
persist-credentials: false
token: ${{ steps.generate_token.outputs.token }}
ref: refs/heads/production
fetch-depth: 0
- name: Configure git credentials
run: git config --global url."https://x-access-token:${{ steps.generate_token.outputs.token }}@github.com/".insteadOf "https://github.com/"
- name: Get target commit hash (manual dispatch)
if: github.event_name == 'workflow_dispatch'
run: |
set -x
# if the target exists on its own use that
if git rev-parse "$TARGET" --quiet; then
target_hash=$(git rev-parse "$TARGET")
fi
# if not try prefixed with origin:
if [ -z "$target_hash" ]; then
target_hash=$(git rev-parse "origin/$TARGET")
fi
echo "SHOULD_DEPLOY=true" >> $GITHUB_ENV
echo "TARGET_HASH=$target_hash" >> $GITHUB_ENV
- name: Get target commit hash (hotfix)
if: github.event_name == 'push'
run: |
set -x
echo "TARGET_HASH=$GITHUB_SHA" >> $GITHUB_ENV
echo "TARGET=hotfix" >> $GITHUB_ENV
# is the hotfix sha already on production?
if git merge-base --is-ancestor "$GITHUB_SHA" production; then
echo "SHOULD_DEPLOY=false" >> $GITHUB_ENV
else
echo "SHOULD_DEPLOY=true" >> $GITHUB_ENV
fi
- name: Author setup (manual dispatch)
if: github.event_name == 'workflow_dispatch'
run: |
set -x
git config --global user.name "${{ github.actor }}"
git config --global user.email 'huppy+${{ github.actor }}@tldraw.com'
- name: Author setup (hotfix)
if: github.event_name == 'push'
run: |
set -x
commit_author_name=$(git log -1 --pretty=format:%cn "$TARGET_HASH")
commit_author_email=$(git log -1 --pretty=format:%ce "$TARGET_HASH")
git config --global user.name "$commit_author_name"
git config --global user.email "$commit_author_email"
- name: Get target tree hash
run: |
set -x
tree_hash=$(git show --quiet --pretty=format:%T "$TARGET_HASH")
echo "TREE_HASH=$tree_hash" >> $GITHUB_ENV
- name: Create commit & update production branch
run: |
set -eux
now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
message="Deploy from $TARGET ($TARGET_HASH) at $now"
current_prod_hash=$(git rev-parse HEAD)
commit=$(git commit-tree -m "$message" -p "$current_prod_hash" -p "$TARGET_HASH" "$TREE_HASH")
git update-ref refs/heads/production "$commit"
git checkout production
- name: Push commit to trigger deployment
if: env.SHOULD_DEPLOY == 'true'
run: |
set -x
git push origin production
# reset hotfixes to the latest production
git push origin production:hotfixes --force

View File

@@ -0,0 +1,70 @@
name: Trigger sdk hotfix
# pull_request_target rather than push/pull_request: hotfix PRs (e.g. the automated
# release-notes updates) carry `[skip ci]` in their commit messages, which suppresses
# push- and pull_request-triggered workflows entirely — the label had no effect.
# GitHub's skip-ci markers do not apply to pull_request_target. The `closed` event
# replaces the old push-to-main trigger (which ran on every merge just to check
# labels), and `labeled` allows re-triggering a hotfix by re-adding the label to an
# already-merged PR. Secrets exposure is safe here: the job only runs for merged PRs
# carrying a maintainer-applied hotfix label, checkout uses the trusted base branch,
# and the only PR-derived code the script builds is the PR's already-merged commit
# from main.
on:
pull_request_target:
types: [closed, labeled]
branches:
- main
defaults:
run:
shell: bash
permissions:
contents: read
jobs:
trigger:
name: Trigger an SDK hotfix
runs-on: ubuntu-latest
concurrency: npm-publish
environment: npm deploy
if: |
github.event.pull_request.merged == true &&
((github.event.action == 'closed' &&
(contains(github.event.pull_request.labels.*.name, 'sdk-hotfix-please') ||
contains(github.event.pull_request.labels.*.name, 'docs-hotfix-please'))) ||
(github.event.action == 'labeled' &&
(github.event.label.name == 'sdk-hotfix-please' || github.event.label.name == 'docs-hotfix-please')))
steps:
- name: Generate a token
id: generate_token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.HUPPY_APP_ID }}
private-key: ${{ secrets.HUPPY_APP_PRIVATE_KEY }}
- uses: actions/checkout@v6
with:
persist-credentials: false
token: ${{ steps.generate_token.outputs.token }}
fetch-depth: 0
- name: Configure git credentials
run: git config --global url."https://x-access-token:${{ steps.generate_token.outputs.token }}@github.com/".insteadOf "https://github.com/"
- uses: ./.github/actions/setup
- name: Maybe trigger sdk hotfix
run: |
set -e
git config --global user.name 'huppy-bot[bot]'
git config --global user.email '128400622+huppy-bot[bot]@users.noreply.github.com'
git log --oneline -10
yarn tsx internal/scripts/trigger-sdk-hotfix.ts
env:
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
TLDRAW_BEMO_URL: https://demo.tldraw.xyz
DISCORD_DEPLOY_WEBHOOK_URL: ${{ secrets.DISCORD_DEPLOY_WEBHOOK_URL }}
PR_NUMBER: ${{ github.event.pull_request.number }}

View File

@@ -0,0 +1,160 @@
name: Update release notes
on:
# Update release notes whenever production is updated, keeping next.mdx (and the
# draft GitHub release synced by the skill) in step with each dotcom release —
# the same production push that deploys dotcom and publishes the `next` package.
# Production only: a push to main must not trigger a notes run on every merge.
# Release-notes PR titles carry `[skip ci]` so their squash-merge commits skip main
# push workflows. Branch commits omit the marker so required PR checks run.
push:
branches:
- production
workflow_dispatch:
concurrency:
group: update-release-notes
defaults:
run:
shell: bash
permissions:
contents: read
jobs:
update-release-notes:
name: 'Update release notes with Claude Code'
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- name: Generate GH token
id: generate_token
uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92
with:
app_id: ${{ secrets.HUPPY_APP_ID }}
private_key: ${{ secrets.HUPPY_PRIVATE_KEY }}
- name: Check out code
uses: actions/checkout@v6
with:
persist-credentials: false
fetch-depth: 0
token: ${{ steps.generate_token.outputs.token }}
- name: Configure git credentials
run: git config --global url."https://x-access-token:${{ steps.generate_token.outputs.token }}@github.com/".insteadOf "https://github.com/"
- name: Run our setup
uses: ./.github/actions/setup
- name: Configure git
run: |
git config --global user.name 'huppy-bot[bot]'
git config --global user.email '128400622+huppy-bot[bot]@users.noreply.github.com'
- name: Create branch
id: branch
run: |
BRANCH_NAME="release-notes/update-$(date +%Y%m%d-%H%M%S)"
git checkout -b "$BRANCH_NAME"
echo "branch_name=$BRANCH_NAME" >> "$GITHUB_OUTPUT"
# Recorded before the skill runs so the close step below can tell an
# already-open PR from an earlier run apart from the one this run opens.
- name: Find open release notes PRs
id: existing_prs
run: |
{
echo 'numbers<<PR_LIST_EOF'
gh pr list --state open --base main --limit 100 --json number,headRefName \
--jq '.[] | select(.headRefName | test("^(release-notes/update-|update-release-notes-)")) | .number'
echo 'PR_LIST_EOF'
} >> "$GITHUB_OUTPUT"
env:
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
- name: Install Claude Code
run: npm install -g @anthropic-ai/claude-code
- name: Run update-release-notes skill
run: |
claude --dangerously-skip-permissions --print "/update-release-notes"
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
- name: Check for changes
id: changes
run: |
if [ -z "$(git status --porcelain)" ]; then
echo "has_changes=false" >> "$GITHUB_OUTPUT"
else
echo "has_changes=true" >> "$GITHUB_OUTPUT"
fi
- name: Commit and push
if: steps.changes.outputs.has_changes == 'true'
run: |
git add -A
git commit -m "Update release notes"
git push -u origin "${{ steps.branch.outputs.branch_name }}"
- name: Create PR
if: steps.changes.outputs.has_changes == 'true'
run: |
gh pr create \
--title "docs(releases): update release notes [skip ci]" \
--body "$(cat <<'EOF'
In order to keep the release notes up to date, this PR adds entries for PRs merged since the last update.
### Change type
- [x] `other`
### Test plan
- [ ] Unit tests
- [ ] End to end tests
EOF
)" \
--base main \
--head "${{ steps.branch.outputs.branch_name }}" \
--label "docs-hotfix-please"
env:
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
# Each run regenerates the notes from scratch, so a PR opened by this run
# supersedes any release notes PR left open by an earlier run. The PR may come
# from the step above or from the skill itself, so this compares the currently
# open PRs against the ones recorded before the skill ran rather than relying on
# a single known PR number. When this run opened nothing, the older PR is still
# the pending update and is left alone.
- name: Close superseded PRs
run: |
EXISTING="${{ steps.existing_prs.outputs.numbers }}"
CURRENT=$(gh pr list --state open --base main --limit 100 --json number,headRefName \
--jq '.[] | select(.headRefName | test("^(release-notes/update-|update-release-notes-)")) | .number')
NEW_PRS=$(comm -13 <(echo "$EXISTING" | sort) <(echo "$CURRENT" | sort))
if [ -z "$NEW_PRS" ]; then
echo "This run did not open a release notes PR, nothing to supersede"
exit 0
fi
# The new PR is already open at this point, so a failure to tidy up an old
# one is a warning, not a reason to fail the run.
NEWEST=$(echo "$NEW_PRS" | sort -n | tail -n 1)
echo "$EXISTING" | while read -r PR; do
[ -n "$PR" ] || continue
echo "Closing superseded release notes PR #$PR"
gh pr close "$PR" --delete-branch --comment "Superseded by #$NEWEST." \
|| echo "Warning: could not close #$PR"
done
env:
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
- name: No changes
if: steps.changes.outputs.has_changes == 'false'
run: echo "No release notes updates needed"