From c422debb621c27b3e749056b5b5cbf1b22fa37a7 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 9 Jan 2025 17:18:33 +0000 Subject: [PATCH] remove huppy-bot (#5189) Describe what your pull request does. If you can, add GIFs or images showing the before and after of your change. ### Change type - [ ] `bugfix` - [ ] `improvement` - [ ] `feature` - [ ] `api` - [x] `other` --- internal/huppy/.gitignore | 42 ---- internal/huppy/Dockerfile | 40 --- internal/huppy/README.md | 80 ------ internal/huppy/fly.toml | 34 --- internal/huppy/next.config.js | 25 -- internal/huppy/package.json | 39 --- internal/huppy/pages/_app.tsx | 13 - internal/huppy/pages/api/dev/getDelivery.ts | 17 -- internal/huppy/pages/api/dev/redeliver.ts | 17 -- internal/huppy/pages/api/dev/simulate.ts | 37 --- internal/huppy/pages/api/github-event.ts | 44 ---- internal/huppy/pages/api/on-release.ts | 35 --- internal/huppy/pages/deliveries.tsx | 164 ------------ internal/huppy/src/Queue.ts | 15 -- internal/huppy/src/comment.tsx | 48 ---- internal/huppy/src/config.tsx | 7 - internal/huppy/src/ctx.tsx | 8 - internal/huppy/src/flow.tsx | 54 ---- .../huppy/src/flows/collectClaSignatures.tsx | 234 ------------------ internal/huppy/src/flows/enforcePrLabels.tsx | 76 ------ internal/huppy/src/flows/index.tsx | 5 - .../src/flows/standaloneExamplesBranch.tsx | 109 -------- internal/huppy/src/getCtxForOrg.tsx | 21 -- internal/huppy/src/octokit.ts | 67 ----- internal/huppy/src/repo.ts | 143 ----------- internal/huppy/src/reportError.tsx | 22 -- internal/huppy/src/requestWrapper.tsx | 16 -- internal/huppy/src/utils.ts | 50 ---- internal/huppy/tsconfig.json | 29 --- internal/scripts/publish-new.ts | 13 - internal/scripts/publish-patch.ts | 15 -- yarn.lock | 35 +-- 32 files changed, 3 insertions(+), 1551 deletions(-) delete mode 100644 internal/huppy/.gitignore delete mode 100644 internal/huppy/Dockerfile delete mode 100644 internal/huppy/README.md delete mode 100644 internal/huppy/fly.toml delete mode 100644 internal/huppy/next.config.js delete mode 100644 internal/huppy/package.json delete mode 100644 internal/huppy/pages/_app.tsx delete mode 100644 internal/huppy/pages/api/dev/getDelivery.ts delete mode 100644 internal/huppy/pages/api/dev/redeliver.ts delete mode 100644 internal/huppy/pages/api/dev/simulate.ts delete mode 100644 internal/huppy/pages/api/github-event.ts delete mode 100644 internal/huppy/pages/api/on-release.ts delete mode 100644 internal/huppy/pages/deliveries.tsx delete mode 100644 internal/huppy/src/Queue.ts delete mode 100644 internal/huppy/src/comment.tsx delete mode 100644 internal/huppy/src/config.tsx delete mode 100644 internal/huppy/src/ctx.tsx delete mode 100644 internal/huppy/src/flow.tsx delete mode 100644 internal/huppy/src/flows/collectClaSignatures.tsx delete mode 100644 internal/huppy/src/flows/enforcePrLabels.tsx delete mode 100644 internal/huppy/src/flows/index.tsx delete mode 100644 internal/huppy/src/flows/standaloneExamplesBranch.tsx delete mode 100644 internal/huppy/src/getCtxForOrg.tsx delete mode 100644 internal/huppy/src/octokit.ts delete mode 100644 internal/huppy/src/repo.ts delete mode 100644 internal/huppy/src/reportError.tsx delete mode 100644 internal/huppy/src/requestWrapper.tsx delete mode 100644 internal/huppy/src/utils.ts delete mode 100644 internal/huppy/tsconfig.json diff --git a/internal/huppy/.gitignore b/internal/huppy/.gitignore deleted file mode 100644 index e81fc5106c6e..000000000000 --- a/internal/huppy/.gitignore +++ /dev/null @@ -1,42 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# PWA build artifacts -/public/*.js - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* - -# local env files -.env*.local - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts - -# Sentry -.sentryclirc diff --git a/internal/huppy/Dockerfile b/internal/huppy/Dockerfile deleted file mode 100644 index 87684e3cb44e..000000000000 --- a/internal/huppy/Dockerfile +++ /dev/null @@ -1,40 +0,0 @@ -# Install dependencies only when needed -FROM node:18.17.0-alpine AS builder -# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. -RUN apk add --no-cache libc6-compat - -RUN corepack enable -WORKDIR /app -COPY . . - -RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn \ - yarn workspaces focus @tldraw/monorepo huppy - -ENV NEXT_TELEMETRY_DISABLED 1 - -WORKDIR /app/internal/huppy -RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn build - -# Production image, copy all the files and run next -FROM node:18.17.0-alpine AS runner - -RUN apk update && apk upgrade && \ - apk add --no-cache bash git openssh - -WORKDIR /app - -RUN corepack enable - -ENV NODE_ENV production -ENV NEXT_TELEMETRY_DISABLED 1 - -RUN addgroup --system --gid 1001 nodejs -RUN adduser --system --uid 1001 nextjs - -COPY --from=builder /app ./ - -USER nextjs - -WORKDIR /app/internal/huppy -CMD ["yarn", "start"] - diff --git a/internal/huppy/README.md b/internal/huppy/README.md deleted file mode 100644 index 88eeda6eb3dd..000000000000 --- a/internal/huppy/README.md +++ /dev/null @@ -1,80 +0,0 @@ -# Huppy - -The huppy-bot app. Huppy-bot is a suite of scripts that helps the tldraw development team automate some processes. - -## Development - -To develop huppy-bot, you'll need to create a .env file that looks like this: - -``` -REPO_SYNC_PRIVATE_KEY_B64= -REPO_SYNC_HOOK_SECRET= -``` - -DM alex to get hold of these credentials. - -To start the server, run `yarn dev-huppy`. Once running, you can go to -http://localhost:3000/deliveries to get to a list of github webhook event -deliveries. To test your code, pick an event that does roughly what you want and -hit 'simulate'. You can also ask GitHub to re-deliver events to the production -version of repo-sync through this UI. - -Huppy-bot isn't currently deployed automatically. To deploy, use: - -```sh -fly deploy --config apps/huppy/fly.toml --dockerfile apps/huppy/Dockerfile -``` - -from the repo root. - -## How it works - -Huppy runs on a server with persistent disk storage attached. It maintains local -mirrors of both our github repos on that disk. When events come in that mean we -need to do some work in a repo, it updates the local mirror, then clones them to -a temporary directory for work. This sort of pull + local clone is _much_ faster -(~1s) than normal from-scratch clones (~1m). - -Huppy's reponsibilities are organized into "flows". These are defined in -`src/flows`. A flow is an object with webhook handlers that implement some -complete set of functionality. Right now there aren't many, but we could add more! - -There's an alternative universe where huppy would exist as a set of github -actions instead. We didn't pursue this route for three reasons: - -1. Huppy needs to operate over multiple github repos at once, which isn't well - supported by actions. -2. Giving actions in our public repo access to our private repo could be a - security risk. We'd have to grant permission to OSS contributors to run - certain actions, which could mean accidentally giving them access to more - than we intend. -3. Having access to the full range of webhook & API options provided by GitHub - means we can create a better DX than would be possible with plain actions - (e.g. the "Fix" button when huppy detects that bublic is out of date). - -It also lets us make use of that local-clone trick, which means huppy responds -to requests in seconds rather than minutes. - -## License - -The code in this folder is Copyright (c) 2024-present tldraw Inc. The tldraw SDK is provided under the [tldraw license](https://github.com/tldraw/tldraw/blob/main/LICENSE.md). - -## Trademarks - -Copyright (c) 2024-present tldraw Inc. The tldraw name and logo are trademarks of tldraw. Please see our [trademark guidelines](https://github.com/tldraw/tldraw/blob/main/TRADEMARKS.md) for info on acceptable usage. - -## Distributions - -You can find tldraw on npm [here](https://www.npmjs.com/package/@tldraw/tldraw?activeTab=versions). - -## Contribution - -Please see our [contributing guide](https://github.com/tldraw/tldraw/blob/main/CONTRIBUTING.md). Found a bug? Please [submit an issue](https://github.com/tldraw/tldraw/issues/new). - -## Community - -Have questions, comments or feedback? [Join our discord](https://discord.gg/rhsyWMUJxd) or [start a discussion](https://github.com/tldraw/tldraw/discussions/new). For the latest news and release notes, visit [tldraw.dev](https://tldraw.dev). - -## Contact - -Find us on Twitter/X at [@tldraw](https://twitter.com/tldraw). diff --git a/internal/huppy/fly.toml b/internal/huppy/fly.toml deleted file mode 100644 index 0b528246ae69..000000000000 --- a/internal/huppy/fly.toml +++ /dev/null @@ -1,34 +0,0 @@ -# fly.toml file generated for tldraw-repo-sync on 2023-04-25T14:25:01+01:00 -app = "tldraw-repo-sync" -kill_signal = "SIGINT" -kill_timeout = 5 -primary_region = "lhr" -processes = [] - -[build] - -[env] -PORT = "8080" - -[mounts] -source = "git_store" -destination = "/tldraw_repo_sync_data" - -[[services]] -internal_port = 8080 -processes = ["app"] -protocol = "tcp" - -[services.concurrency] -hard_limit = 25 -soft_limit = 20 -type = "connections" - -[[services.ports]] -force_https = true -handlers = ["http"] -port = 80 - -[[services.ports]] -handlers = ["tls", "http"] -port = 443 diff --git a/internal/huppy/next.config.js b/internal/huppy/next.config.js deleted file mode 100644 index d1105fd60556..000000000000 --- a/internal/huppy/next.config.js +++ /dev/null @@ -1,25 +0,0 @@ -/** @type {import('next').NextConfig} */ -const nextConfig = { - reactStrictMode: true, - swcMinify: true, - transpilePackages: [], - productionBrowserSourceMaps: true, - webpack: (config, context) => { - config.module.rules.push({ - test: /\.(svg|json|woff2)$/, - type: 'asset/resource', - }) - return config - }, - redirects: async () => { - return [{ source: '/', destination: 'https://www.tldraw.com/', permanent: false }] - }, - eslint: { - ignoreDuringBuilds: true, - }, - typescript: { - ignoreBuildErrors: true, - }, -} - -module.exports = nextConfig diff --git a/internal/huppy/package.json b/internal/huppy/package.json deleted file mode 100644 index 13b4d55db7d8..000000000000 --- a/internal/huppy/package.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "name": "huppy", - "description": "Tools for managing our public and private repos", - "version": "0.0.0", - "private": true, - "author": { - "name": "tldraw GB Ltd.", - "email": "hello@tldraw.com" - }, - "homepage": "https://tldraw.dev", - "browserslist": [ - "defaults" - ], - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "yarn run -T tsx ../scripts/lint.ts" - }, - "dependencies": { - "@octokit/core": "^5.0.1", - "@octokit/plugin-retry": "^6.0.1", - "@octokit/webhooks-types": "^6.11.0", - "@tldraw/utils": "workspace:*", - "@tldraw/validate": "workspace:*", - "@types/jsonwebtoken": "^9.0.1", - "json5": "^2.2.3", - "jsonwebtoken": "^9.0.0", - "next": "^14.0.4", - "octokit": "^3.1.1", - "react": "^18.2.0", - "react-dom": "^18.2.0" - }, - "devDependencies": { - "eslint": "^9.13.0", - "eslint-config-next": "^15.0.0", - "lazyrepo": "0.0.0-alpha.27" - } -} diff --git a/internal/huppy/pages/_app.tsx b/internal/huppy/pages/_app.tsx deleted file mode 100644 index 227178e9eeec..000000000000 --- a/internal/huppy/pages/_app.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import type { AppProps } from 'next/app' -import Head from 'next/head' - -export default function MyApp({ Component, pageProps }: AppProps) { - return ( - <> - - - - - - ) -} diff --git a/internal/huppy/pages/api/dev/getDelivery.ts b/internal/huppy/pages/api/dev/getDelivery.ts deleted file mode 100644 index 530583a71395..000000000000 --- a/internal/huppy/pages/api/dev/getDelivery.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { assert } from '@tldraw/utils' -import { NextApiRequest, NextApiResponse } from 'next' -import { getAppOctokit } from '../../../src/octokit' - -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - assert(process.env.NODE_ENV !== 'production') - assert(req.method === 'GET') - const id = req.query.id - assert(typeof id === 'string') - - const gh = getAppOctokit() - const { data: delivery } = await gh.octokit.rest.apps.getWebhookDelivery({ - delivery_id: Number(id), - }) - - return res.json(delivery) -} diff --git a/internal/huppy/pages/api/dev/redeliver.ts b/internal/huppy/pages/api/dev/redeliver.ts deleted file mode 100644 index 9e2a9725e02b..000000000000 --- a/internal/huppy/pages/api/dev/redeliver.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { assert } from '@tldraw/utils' -import { NextApiRequest, NextApiResponse } from 'next' -import { getAppOctokit } from '../../../src/octokit' - -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - assert(process.env.NODE_ENV !== 'production') - assert(req.method === 'POST') - const deliveryId = req.body.id as number - assert(typeof deliveryId === 'number') - - const gh = getAppOctokit() - await gh.octokit.rest.apps.redeliverWebhookDelivery({ - delivery_id: deliveryId, - }) - - return res.json({ ok: true }) -} diff --git a/internal/huppy/pages/api/dev/simulate.ts b/internal/huppy/pages/api/dev/simulate.ts deleted file mode 100644 index 743c7b315371..000000000000 --- a/internal/huppy/pages/api/dev/simulate.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { assert, assertExists } from '@tldraw/utils' -import { NextApiRequest, NextApiResponse } from 'next' -import { Ctx } from '../../../src/ctx' -import { NamedEvent, onGithubEvent } from '../../../src/flow' -import { getAppOctokit, getInstallationToken } from '../../../src/octokit' - -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - assert(process.env.NODE_ENV !== 'production') - assert(req.method === 'POST') - const deliveryId = req.body.id as number - assert(typeof deliveryId === 'number') - - const app = getAppOctokit() - - const { data: delivery } = await app.octokit.rest.apps.getWebhookDelivery({ - delivery_id: deliveryId, - }) - - const installationId = assertExists(delivery.installation_id) - const ctx: Ctx = { - app, - installationId, - octokit: await app.getInstallationOctokit(installationId), - installationToken: await getInstallationToken(app, installationId), - } - - try { - const messages = await onGithubEvent(ctx, { - name: delivery.event, - payload: delivery.request.payload, - } as NamedEvent) - return res.json({ message: JSON.stringify(messages, null, '\t') }) - } catch (err: any) { - console.log(err.stack) - return res.json({ message: err.message }) - } -} diff --git a/internal/huppy/pages/api/github-event.ts b/internal/huppy/pages/api/github-event.ts deleted file mode 100644 index 57c81c5cecc7..000000000000 --- a/internal/huppy/pages/api/github-event.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { WebhookEventName } from '@octokit/webhooks-types' -import { assert } from '@tldraw/utils' -import { NextApiRequest, NextApiResponse } from 'next' -import { Ctx } from '../../src/ctx' -import { NamedEvent, onGithubEvent } from '../../src/flow' -import { getAppOctokit, getInstallationToken } from '../../src/octokit' -import { wrapRequest } from '../../src/requestWrapper' -import { header } from '../../src/utils' - -const handler = wrapRequest( - '/api/github-event', - async function handler(req: NextApiRequest, res: NextApiResponse) { - const app = getAppOctokit() - const eventName = header(req, 'x-github-event') as WebhookEventName - await app.webhooks.verifyAndReceive({ - id: header(req, 'x-github-delivery'), - name: eventName, - signature: header(req, 'x-hub-signature-256'), - payload: JSON.stringify(req.body), - }) - - const event = { name: eventName, payload: req.body } as NamedEvent - assert( - 'installation' in event.payload && event.payload.installation, - 'event must have installation' - ) - - const installationId = event.payload.installation.id - const ctx: Ctx = { - app, - octokit: await app.getInstallationOctokit(Number(installationId)), - installationId: installationId, - installationToken: await getInstallationToken(app, installationId), - } - - // we deliberately don't await this so that the response is sent - // immediately. we'll process the event in the background. - onGithubEvent(ctx, event) - - return res.json({ ok: true }) - } -) - -export default handler diff --git a/internal/huppy/pages/api/on-release.ts b/internal/huppy/pages/api/on-release.ts deleted file mode 100644 index ee06bda9406b..000000000000 --- a/internal/huppy/pages/api/on-release.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { assert } from '@tldraw/utils' -import { T } from '@tldraw/validate' -import { NextApiRequest, NextApiResponse } from 'next' -import { TLDRAW_ORG } from '../../src/config' -import { standaloneExamplesBranch } from '../../src/flows/standaloneExamplesBranch' -import { getCtxForOrg } from '../../src/getCtxForOrg' -import { wrapRequest } from '../../src/requestWrapper' - -const bodySchema = T.object({ - tagToRelease: T.string, - apiKey: T.string, - canary: T.boolean.optional(), -}) - -const handler = wrapRequest( - '/api/on-release', - async function handler(req: NextApiRequest, res: NextApiResponse) { - assert(req.method === 'POST') - const body = bodySchema.validate(req.body) - assert(typeof process.env.DEVELOPER_ACCESS_KEY === 'string') - if (body.apiKey !== process.env.DEVELOPER_ACCESS_KEY) { - res.status(401).send('Bad api key') - return - } - - const { tagToRelease } = body - await standaloneExamplesBranch.onCustomHook(await getCtxForOrg(TLDRAW_ORG), { - tagToRelease, - canary: !!body.canary, - }) - res.send('Created standalone examples branch') - } -) - -export default handler diff --git a/internal/huppy/pages/deliveries.tsx b/internal/huppy/pages/deliveries.tsx deleted file mode 100644 index a3f8c940047e..000000000000 --- a/internal/huppy/pages/deliveries.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import { assert } from '@tldraw/utils' -import { GetServerSidePropsContext } from 'next' -import { useEffect, useState } from 'react' -import { getAppOctokit } from '../src/octokit' - -interface Props { - deliveries: { - id: number - guid: string - delivered_at: string - redelivery: boolean - duration: number - status: string - status_code: number - event: string - action: string | null - installation_id: number | null - repository_id: number | null - }[] - cursor: string | null -} - -export async function getServerSideProps(context: GetServerSidePropsContext) { - assert(process.env.NODE_ENV !== 'production') - - const gh = getAppOctokit() - const deliveries = await gh.octokit.rest.apps.listWebhookDeliveries({ - per_page: 100, - cursor: (context.query.cursor as string) ?? undefined, - }) - - const nextLinkMatch = deliveries.headers.link?.match(/(?<=<)([\S]*)(?=>; rel="Next")/i) - let cursor: string | null = null - if (nextLinkMatch) { - const url = new URL(nextLinkMatch[0]) - cursor = url.searchParams.get('cursor') - } - - return { props: { deliveries: deliveries.data, cursor } } -} - -interface SelectedDelivery { - id: number - data?: unknown -} - -export default function Deliveries({ deliveries, cursor }: Props) { - const [selectedDelivery, setSelectedDelivery] = useState(null) - const [isSimulating, setIsSimulating] = useState(false) - const [isRedelivering, setIsRedelivering] = useState(false) - - useEffect(() => { - if (!selectedDelivery || (selectedDelivery && selectedDelivery.data)) return - - let cancelled = false - ;(async () => { - const response = await fetch(`/api/dev/getDelivery?id=${selectedDelivery.id}`) - const data = await response.json() - if (cancelled) return - setSelectedDelivery({ id: selectedDelivery.id, data }) - })() - - return () => { - cancelled = true - } - }) - - return ( -
-

Deliveries

-
-
    - {deliveries.map((delivery) => ( -
  1. setSelectedDelivery({ id: delivery.id })} - > -
    - {delivery.event} - {delivery.action && `.${delivery.action}`} -
    -
    {formatDate(new Date(delivery.delivered_at))}
    -
  2. - ))} -
  3. - - Load more... - -
  4. -
- {selectedDelivery && selectedDelivery.data ? ( -
-
-							{JSON.stringify(selectedDelivery.data, null, '\t')}
-						
-
- - -
-
- ) : selectedDelivery ? ( -
- loading... -
- ) : null} -
-
- ) -} - -function formatDate(date: Date) { - const intl = new Intl.DateTimeFormat('en-GB', { - dateStyle: 'short', - timeStyle: 'short', - }) - - return intl.format(date) -} diff --git a/internal/huppy/src/Queue.ts b/internal/huppy/src/Queue.ts deleted file mode 100644 index 749e21f3ec83..000000000000 --- a/internal/huppy/src/Queue.ts +++ /dev/null @@ -1,15 +0,0 @@ -export class Queue { - currentTask = Promise.resolve() - - enqueue(task: () => Promise): Promise { - return new Promise((resolve, reject) => { - this.currentTask = this.currentTask.then(async () => { - try { - resolve(await task()) - } catch (err) { - reject(err) - } - }) - }) - } -} diff --git a/internal/huppy/src/comment.tsx b/internal/huppy/src/comment.tsx deleted file mode 100644 index 5ce40a402c6a..000000000000 --- a/internal/huppy/src/comment.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { APP_USER_NAME, TLDRAW_ORG, TLDRAW_PUBLIC_REPO } from './config' -import { Ctx } from './ctx' - -export async function findHuppyCommentIfExists(ctx: Ctx, prNumber: number) { - const { data: comments } = await ctx.octokit.rest.issues.listComments({ - owner: TLDRAW_ORG, - repo: TLDRAW_PUBLIC_REPO, - issue_number: prNumber, - per_page: 100, - sort: 'created', - direction: 'asc', - }) - - const foundComment = comments.find((comment) => comment.user?.login === APP_USER_NAME) - - return foundComment ?? null -} - -export async function updateHuppyCommentIfExists(ctx: Ctx, prNumber: number, body: string) { - const foundComment = await findHuppyCommentIfExists(ctx, prNumber) - if (foundComment) { - await ctx.octokit.rest.issues.updateComment({ - owner: TLDRAW_ORG, - repo: TLDRAW_PUBLIC_REPO, - comment_id: foundComment.id, - body, - }) - } -} - -export async function createOrUpdateHuppyComment(ctx: Ctx, prNumber: number, body: string) { - const foundComment = await findHuppyCommentIfExists(ctx, prNumber) - if (foundComment) { - await ctx.octokit.rest.issues.updateComment({ - owner: TLDRAW_ORG, - repo: TLDRAW_PUBLIC_REPO, - comment_id: foundComment.id, - body, - }) - } else { - await ctx.octokit.rest.issues.createComment({ - owner: TLDRAW_ORG, - repo: TLDRAW_PUBLIC_REPO, - issue_number: prNumber, - body, - }) - } -} diff --git a/internal/huppy/src/config.tsx b/internal/huppy/src/config.tsx deleted file mode 100644 index 6a5a75b9746d..000000000000 --- a/internal/huppy/src/config.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export const APP_USER_EMAIL = '128400622+huppy-bot[bot]@users.noreply.github.com' -export const APP_USER_NAME = 'huppy-bot[bot]' -export const APP_ID = '307634' - -export const TLDRAW_ORG = 'tldraw' -export const TLDRAW_PUBLIC_REPO = 'tldraw' -export const TLDRAW_PUBLIC_REPO_MAIN_BRANCH = 'main' diff --git a/internal/huppy/src/ctx.tsx b/internal/huppy/src/ctx.tsx deleted file mode 100644 index 544005aa2f61..000000000000 --- a/internal/huppy/src/ctx.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { App, Octokit } from 'octokit' - -export interface Ctx { - app: App - octokit: Octokit - installationToken: string - installationId: number -} diff --git a/internal/huppy/src/flow.tsx b/internal/huppy/src/flow.tsx deleted file mode 100644 index 8ab7133fe99e..000000000000 --- a/internal/huppy/src/flow.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { WebhookEventMap } from '@octokit/webhooks-types' -import { Ctx } from './ctx' -import { allFlows } from './flows' -import { reportError } from './reportError' -import { camelCase, capitalize, elapsed } from './utils' - -type CamelCase = S extends `${infer P1}_${infer P2}${infer P3}` - ? `${Lowercase}${Uppercase}${CamelCase}` - : Lowercase - -export type NamedEvent = { - [Name in keyof WebhookEventMap]: { name: Name; payload: WebhookEventMap[Name] } -}[keyof WebhookEventMap] - -export type Flow = { - name: string - onCustomHook?(ctx: Ctx, payload: CustomHookPayload): Promise -} & { - [Name in keyof WebhookEventMap as `on${Capitalize>}`]?: ( - ctx: Ctx, - payload: WebhookEventMap[Name] - ) => Promise -} - -export async function onGithubEvent(ctx: Ctx, event: NamedEvent) { - let nameString = event.name - if ('action' in event.payload) { - nameString += `.${event.payload.action}` - } - console.log('Starting event:', nameString) - const handlerName = `on${capitalize(camelCase(event.name))}` as `on${Capitalize< - CamelCase - >}` - - const results: Record = {} - - for (const flow of allFlows) { - if (handlerName in flow) { - const actionName = `${flow.name}.${handlerName}` - const start = Date.now() - try { - console.log(`===== Starting ${actionName} =====`) - await (flow as any)[handlerName](ctx, event.payload) - console.log(`===== Finished ${actionName} in ${elapsed(start)} =====`) - results[actionName] = `ok in ${elapsed(start)}` - } catch (err: any) { - results[actionName] = err.message - await reportError(`Error in ${flow.name}.${handlerName}`, err) - } - } - } - - return results -} diff --git a/internal/huppy/src/flows/collectClaSignatures.tsx b/internal/huppy/src/flows/collectClaSignatures.tsx deleted file mode 100644 index aa92cfef843e..000000000000 --- a/internal/huppy/src/flows/collectClaSignatures.tsx +++ /dev/null @@ -1,234 +0,0 @@ -import { IssueCommentEvent } from '@octokit/webhooks-types' -import { assert } from '@tldraw/utils' -import { createOrUpdateHuppyComment, updateHuppyCommentIfExists } from '../comment' -import { TLDRAW_ORG, TLDRAW_PUBLIC_REPO } from '../config' -import { Ctx } from '../ctx' -import { Flow } from '../flow' - -const TARGET_REPO = TLDRAW_PUBLIC_REPO -const CLA_URL = - 'https://tldraw.notion.site/Contributor-License-Agreement-4d529dd5e4b3438b90cdf2a2f9d7e7e6?pvs=4' -const SIGNING_MESSAGE = 'I have read and agree to the Contributor License Agreement.' -const RE_CHECK_MESSAGE = '/huppy check cla' -const CLA_SIGNATURES_BRANCH = 'cla-signees' - -const pullRequestActionsToCheck = ['opened', 'synchronize', 'reopened', 'edited'] - -interface Signing { - githubId: number - signedAt: string - signedVersion: 1 - signingComment: string -} -interface SigneeInfo { - unsigned: Set - signees: Map - total: number -} - -export const collectClaSignatures: Flow = { - name: 'collectClaSignatures', - - onPullRequest: async (ctx, event) => { - if (event.repository.full_name !== `${TLDRAW_ORG}/${TARGET_REPO}`) return - if (!pullRequestActionsToCheck.includes(event.action)) return - - await checkAllContributorsHaveSignedCla(ctx, event.pull_request) - }, - - onIssueComment: async (ctx, event) => { - if (event.repository.full_name !== `${TLDRAW_ORG}/${TARGET_REPO}`) return - if (event.issue.pull_request === undefined) return - - switch (event.comment.body.trim().toLowerCase()) { - case SIGNING_MESSAGE.toLowerCase(): - await addSignatureFromComment(ctx, event) - break - case RE_CHECK_MESSAGE.toLowerCase(): { - const pr = await ctx.octokit.rest.pulls.get({ - owner: TLDRAW_ORG, - repo: TARGET_REPO, - pull_number: event.issue.number, - }) - await checkAllContributorsHaveSignedCla(ctx, pr.data) - await ctx.octokit.rest.reactions.createForIssueComment({ - owner: TLDRAW_ORG, - repo: TARGET_REPO, - comment_id: event.comment.id, - content: '+1', - }) - break - } - } - }, -} - -async function addSignatureFromComment(ctx: Ctx, event: IssueCommentEvent) { - const existingSignature = await getClaSigneeInfo(ctx, event.comment.user.login.toLowerCase()) - if (existingSignature) { - await ctx.octokit.rest.reactions.createForIssueComment({ - owner: TLDRAW_ORG, - repo: TARGET_REPO, - comment_id: event.comment.id, - content: 'heart', - }) - return - } - - const newSigning: Signing = { - githubId: event.comment.user.id, - signedAt: event.comment.created_at, - signedVersion: 1, - signingComment: event.comment.html_url, - } - - await ctx.octokit.rest.repos.createOrUpdateFileContents({ - owner: TLDRAW_ORG, - repo: TLDRAW_PUBLIC_REPO, - path: `${event.comment.user.login.toLowerCase()}.json`, - branch: CLA_SIGNATURES_BRANCH, - content: Buffer.from(JSON.stringify(newSigning, null, '\t')).toString('base64'), - message: `Add CLA signature for ${event.comment.user.login}`, - }) - - const pr = await ctx.octokit.rest.pulls.get({ - owner: TLDRAW_ORG, - repo: TARGET_REPO, - pull_number: event.issue.number, - }) - await checkAllContributorsHaveSignedCla(ctx, pr.data) - - await ctx.octokit.rest.reactions.createForIssueComment({ - owner: TLDRAW_ORG, - repo: TARGET_REPO, - comment_id: event.comment.id, - content: 'heart', - }) -} - -async function checkAllContributorsHaveSignedCla( - ctx: Ctx, - pr: { head: { sha: string }; number: number } -) { - const info = await getClaSigneesFromPr(ctx, pr) - - if (info.unsigned.size === 0) { - await ctx.octokit.rest.repos.createCommitStatus({ - owner: TLDRAW_ORG, - repo: TARGET_REPO, - sha: pr.head.sha, - state: 'success', - context: 'CLA Signatures', - description: getStatusDescription(info), - }) - await updateHuppyCommentIfExists(ctx, pr.number, getHuppyCommentContents(info)) - return - } - - await ctx.octokit.rest.repos.createCommitStatus({ - owner: TLDRAW_ORG, - repo: TARGET_REPO, - sha: pr.head.sha, - state: 'failure', - context: 'CLA Signatures', - description: getStatusDescription(info), - }) - - await createOrUpdateHuppyComment(ctx, pr.number, getHuppyCommentContents(info)) -} - -async function getClaSigneesFromPr(ctx: Ctx, pr: { number: number }): Promise { - const allAuthors = new Set() - - const commits = await ctx.octokit.paginate( - 'GET /repos/{owner}/{repo}/pulls/{pull_number}/commits', - { - owner: TLDRAW_ORG, - repo: TARGET_REPO, - pull_number: pr.number, - } - ) - - for (const commit of commits) { - if (commit.author && !commit.author.login.endsWith('[bot]')) { - allAuthors.add(commit.author.login.toLowerCase()) - } - } - - const signees = new Map() - const unsigned = new Set() - for (const author of [...allAuthors].sort()) { - const signeeInfo = await getClaSigneeInfo(ctx, author) - if (signeeInfo) { - signees.set(author, signeeInfo) - } else { - unsigned.add(author) - } - } - - return { signees, unsigned, total: allAuthors.size } -} - -async function getClaSigneeInfo(ctx: Ctx, authorName: string) { - try { - const response = await ctx.octokit.rest.repos.getContent({ - owner: TLDRAW_ORG, - repo: TLDRAW_PUBLIC_REPO, - path: `${authorName}.json`, - ref: 'cla-signees', - }) - assert(!Array.isArray(response.data), 'Expected a file, not a directory') - assert(response.data.type === 'file', 'Expected a file, not a directory') - return { - signing: JSON.parse( - Buffer.from(response.data.content, 'base64').toString('utf-8') - ) as Signing, - fileSha: response.data.sha, - } - } catch (err: any) { - if (err.status === 404) { - return null - } - throw err - } -} - -function getHuppyCommentContents(info: SigneeInfo) { - if (info.signees.size > 1) { - let listing = `**${info.signees.size}** out of **${info.total}** ${ - info.total === 1 ? 'authors has' : 'authors have' - } signed the [CLA](${CLA_URL}).\n\n` - - for (const author of info.unsigned) { - listing += `- [ ] @${author}\n` - } - for (const author of info.signees.keys()) { - listing += `- [x] @${author}\n` - } - - if (info.unsigned.size === 0) { - return `${listing}\n\nThanks!` - } - - return `Hey, thanks for your pull request! Before we can merge your PR, each author will need to sign our [Contributor License Agreement](${CLA_URL}) by posting a comment that reads: - -> ${SIGNING_MESSAGE} ---- - -${listing}` - } else { - const author = [...info.signees.keys()][0] - - if (info.unsigned.size === 0) { - return `**${author}** has signed the [Contributor License Agreement](${CLA_URL}). Thanks!` - } - - return `Hey, thanks for your pull request! Before we can merge your PR, you will need to sign our [Contributor License Agreement](${CLA_URL}) by posting a comment that reads: - -> ${SIGNING_MESSAGE}` - } -} - -function getStatusDescription(info: SigneeInfo) { - return `${info.signees.size}/${info.total} signed. Comment '${RE_CHECK_MESSAGE}' to re-check.` -} diff --git a/internal/huppy/src/flows/enforcePrLabels.tsx b/internal/huppy/src/flows/enforcePrLabels.tsx deleted file mode 100644 index 366b1bbab4fe..000000000000 --- a/internal/huppy/src/flows/enforcePrLabels.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { TLDRAW_ORG, TLDRAW_PUBLIC_REPO, TLDRAW_PUBLIC_REPO_MAIN_BRANCH } from '../config' -import { Flow } from '../flow' - -export const enforcePrLabels: Flow = { - name: 'enforcePrLabels', - async onPullRequest(ctx, event) { - if (event.repository.full_name !== `${TLDRAW_ORG}/${TLDRAW_PUBLIC_REPO}`) return - if (event.pull_request.base.ref !== TLDRAW_PUBLIC_REPO_MAIN_BRANCH) return - - const fail = async (message: string) => { - await ctx.octokit.rest.repos.createCommitStatus({ - owner: event.repository.owner.login, - repo: event.repository.name, - sha: event.pull_request.head.sha, - state: 'failure', - description: message, - context: 'Release Label', - }) - } - - const succeed = async (message: string) => { - await ctx.octokit.rest.repos.createCommitStatus({ - owner: event.repository.owner.login, - repo: event.repository.name, - sha: event.pull_request.head.sha, - state: 'success', - description: message, - context: 'Release Label', - }) - } - - const pull = event.pull_request - - if (pull.draft) { - return await succeed('Draft PR, skipping label check') - } - - if (pull.closed_at || pull.merged_at) { - return await succeed('Closed PR, skipping label check') - } - - const availableLabels = ( - await ctx.octokit.rest.issues.listLabelsForRepo({ - owner: event.repository.owner.login, - repo: event.repository.name, - }) - ).data.map((x) => x.name) - - const prBody = pull.body - - const selectedReleaseLabels = availableLabels.filter((label) => - prBody?.match(new RegExp(`^\\s*?-\\s*\\[\\s*x\\s*\\]\\s+\`${label}\``, 'm')) - ) as string[] - - if (selectedReleaseLabels.length === 0 && pull.labels.length === 0) { - return fail('Please add a label to the PR.') - } - - if (pull.body?.includes('Fixed a bug with…')) { - return fail('Add a release note to the PR body.') - } - - // add any labels that are checked - console.log('adding labels') - if (selectedReleaseLabels.length > 0) { - await ctx.octokit.rest.issues.addLabels({ - issue_number: pull.number, - owner: event.repository.organization ?? event.repository.owner.login, - repo: event.repository.name, - labels: selectedReleaseLabels, - } as any) - } - - return await succeed(`PR is labelled!`) - }, -} diff --git a/internal/huppy/src/flows/index.tsx b/internal/huppy/src/flows/index.tsx deleted file mode 100644 index 15ec9e47f512..000000000000 --- a/internal/huppy/src/flows/index.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { collectClaSignatures } from './collectClaSignatures' -import { enforcePrLabels } from './enforcePrLabels' -import { standaloneExamplesBranch } from './standaloneExamplesBranch' - -export const allFlows = [enforcePrLabels, standaloneExamplesBranch, collectClaSignatures] diff --git a/internal/huppy/src/flows/standaloneExamplesBranch.tsx b/internal/huppy/src/flows/standaloneExamplesBranch.tsx deleted file mode 100644 index 4f04d417ee3c..000000000000 --- a/internal/huppy/src/flows/standaloneExamplesBranch.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { assert } from '@tldraw/utils' -import * as fs from 'fs/promises' -import * as path from 'path' -import { Flow } from '../flow' -import { withWorkingRepo } from '../repo' -import { readJsonIfExists } from '../utils' - -const filesToCopyFromRoot = ['.gitignore', '.prettierrc', 'LICENSE.md'] -const packageDepsToSyncFromRoot = ['typescript', '@types/react', '@types/react-dom'] - -export const standaloneExamplesBranch = { - name: 'standaloneExamplesBranch', - - onCustomHook: async (ctx, event) => { - await withWorkingRepo( - 'public', - ctx.installationToken, - event.tagToRelease, - async ({ git, repoPath }) => { - const standaloneExamplesWorkDir = path.join(repoPath, '.git', 'standalone-examples') - - const currentCommitHash = await git.trimmed('rev-parse', 'HEAD') - const branchName = `standalone-examples-${currentCommitHash}` - await git('checkout', '-b', branchName) - - // copy examples into new folder - console.log('Copying examples into new folder...') - for (const file of await git.lines('ls-files', 'apps/examples')) { - const relativePath = path.relative('apps/examples', file) - await fs.mkdir(path.join(standaloneExamplesWorkDir, path.dirname(relativePath)), { - recursive: true, - }) - await fs.copyFile( - path.join(repoPath, file), - path.join(standaloneExamplesWorkDir, relativePath) - ) - } - - for (const file of filesToCopyFromRoot) { - await fs.copyFile(path.join(repoPath, file), path.join(standaloneExamplesWorkDir, file)) - } - - console.log('Creation tsconfig.json...') - const tsconfig = await readJsonIfExists( - path.join(repoPath, 'internal/config/tsconfig.base.json') - ) - tsconfig.includes = ['src'] - await fs.writeFile( - path.join(standaloneExamplesWorkDir, 'tsconfig.json'), - JSON.stringify(tsconfig, null, '\t') - ) - - console.log('Creation package.json...') - const rootPackageJson = await readJsonIfExists(path.join(repoPath, 'package.json')) - const examplesPackageJson = await readJsonIfExists( - path.join(standaloneExamplesWorkDir, 'package.json') - ) - - for (const dep of packageDepsToSyncFromRoot) { - examplesPackageJson.dependencies[dep] = rootPackageJson.dependencies[dep] - } - - for (const name of Object.keys( - examplesPackageJson.dependencies as Record - )) { - if (!name.startsWith('@tldraw/')) continue - const packageJsonFile = await readJsonIfExists( - path.join(repoPath, 'packages', name.replace('@tldraw/', ''), 'package.json') - ) - assert(packageJsonFile, `package.json for ${name} must exist`) - if (event.canary) { - const baseVersion = packageJsonFile.version.replace(/-.*$/, '') - const canaryTag = `canary.${currentCommitHash.slice(0, 12)}` - examplesPackageJson.dependencies[name] = `${baseVersion}-${canaryTag}` - } else { - examplesPackageJson.dependencies[name] = packageJsonFile.version - } - } - - await fs.writeFile( - path.join(standaloneExamplesWorkDir, 'package.json'), - JSON.stringify(examplesPackageJson, null, '\t') - ) - - console.log('Deleting existing repo contents...') - for (const file of await fs.readdir(repoPath)) { - if (file === '.git') continue - await fs.rm(path.join(repoPath, file), { recursive: true, force: true }) - } - - console.log('Moving new repo contents into place...') - for (const file of await fs.readdir(standaloneExamplesWorkDir)) { - await fs.rename(path.join(standaloneExamplesWorkDir, file), path.join(repoPath, file)) - } - - await fs.rm(standaloneExamplesWorkDir, { recursive: true, force: true }) - - console.log('Committing & pushing changes...') - await git('add', '-A') - await git( - 'commit', - '-m', - `[automated] Update standalone examples from ${event.tagToRelease}` - ) - await git('push', '--force', 'origin', `${branchName}:examples`) - } - ) - }, -} satisfies Flow<{ tagToRelease: string; canary: boolean }> diff --git a/internal/huppy/src/getCtxForOrg.tsx b/internal/huppy/src/getCtxForOrg.tsx deleted file mode 100644 index 24ab4c2e707b..000000000000 --- a/internal/huppy/src/getCtxForOrg.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Ctx } from './ctx' -import { getAppOctokit, getInstallationToken } from './octokit' - -export async function getCtxForOrg(orgName: string): Promise { - const app = getAppOctokit() - - for await (const { installation } of app.eachInstallation.iterator()) { - if (!installation.account) continue - if (!('login' in installation.account)) continue - if (installation.account.login !== orgName) continue - - return { - app, - installationId: installation.id, - octokit: await app.getInstallationOctokit(installation.id), - installationToken: await getInstallationToken(app, installation.id), - } - } - - throw new Error(`No installation found for org ${orgName}`) -} diff --git a/internal/huppy/src/octokit.ts b/internal/huppy/src/octokit.ts deleted file mode 100644 index 619ecaaedafd..000000000000 --- a/internal/huppy/src/octokit.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Octokit as OctokitCore } from '@octokit/core' -import { retry } from '@octokit/plugin-retry' -import { assert } from '@tldraw/utils' -import console from 'console' -import { App, Octokit } from 'octokit' -import { APP_ID } from './config' - -export function getGitHubAuth() { - const REPO_SYNC_PRIVATE_KEY_B64 = process.env.REPO_SYNC_PRIVATE_KEY_B64 - assert( - typeof REPO_SYNC_PRIVATE_KEY_B64 === 'string', - 'REPO_SYNC_PRIVATE_KEY_B64 must be a string' - ) - const REPO_SYNC_PRIVATE_KEY = Buffer.from(REPO_SYNC_PRIVATE_KEY_B64, 'base64').toString('utf-8') - - const REPO_SYNC_HOOK_SECRET = process.env.REPO_SYNC_HOOK_SECRET - assert(typeof REPO_SYNC_HOOK_SECRET === 'string', 'REPO_SYNC_HOOK_SECRET must be a string') - - return { - privateKey: REPO_SYNC_PRIVATE_KEY, - webhookSecret: REPO_SYNC_HOOK_SECRET, - } -} - -export async function getInstallationToken(gh: App, installationId: number) { - const { data } = await gh.octokit.rest.apps.createInstallationAccessToken({ - installation_id: installationId, - } as any) - return data.token -} - -function requestLogPlugin(octokit: OctokitCore) { - octokit.hook.wrap('request', async (request, options) => { - const url = options.url.replace(/{([^}]+)}/g, (_, key) => (options as any)[key]) - let info = `${options.method} ${url}` - if (options.request.retryCount) { - info += ` (retry ${options.request.retryCount})` - } - - try { - const result = await request(options) - console.log(`[gh] ${result.status} ${info}`) - return result - } catch (err: any) { - console.log(`[gh] ${err.status} ${info}`) - throw err - } - }) -} - -const OctokitWithRetry = Octokit.plugin(requestLogPlugin, retry).defaults({ - retry: { - // retry on 404s, which can occur if we make a request for a resource before it's ready - doNotRetry: [400, 401, 403, 422, 451], - }, -}) - -export function getAppOctokit() { - const { privateKey, webhookSecret } = getGitHubAuth() - return new App({ - Octokit: OctokitWithRetry, - appId: APP_ID, - privateKey, - webhooks: { secret: webhookSecret }, - log: console, - }) -} diff --git a/internal/huppy/src/repo.ts b/internal/huppy/src/repo.ts deleted file mode 100644 index 45f06000ad08..000000000000 --- a/internal/huppy/src/repo.ts +++ /dev/null @@ -1,143 +0,0 @@ -import * as fs from 'fs/promises' -import * as os from 'os' -import * as path from 'path' -import { exec } from '../../../internal/scripts/lib/exec' -import { Queue } from './Queue' -import { APP_USER_EMAIL, APP_USER_NAME, TLDRAW_ORG, TLDRAW_PUBLIC_REPO } from './config' - -const globalGitQueue = new Queue() - -const repos = { - public: { - org: TLDRAW_ORG, - name: TLDRAW_PUBLIC_REPO, - path: 'tldraw-public', - queue: new Queue(), - }, -} as const - -export function prefixOutput(prefix: string) { - return { - processStdoutLine: (line: string) => process.stdout.write(`${prefix}${line}\n`), - processStderrLine: (line: string) => process.stderr.write(`${prefix}${line}\n`), - } -} - -export function createGit(pwd: string) { - const git = async (command: string, ...args: (string | null)[]) => - exec('git', [command, ...args], { pwd, ...prefixOutput(`[git ${command}] `) }) - - git.trimmed = async (command: string, ...args: (string | null)[]) => - (await git(command, ...args)).trim() - - git.lines = async (command: string, ...args: (string | null)[]) => - (await git(command, ...args)).trim().split('\n') - - git.cd = (dir: string) => createGit(path.join(pwd, dir)) - - return git -} - -export type Git = ReturnType - -export async function getPersistentDataPath() { - try { - await fs.writeFile('/tldraw_repo_sync_data/check', 'ok') - return '/tldraw_repo_sync_data' - } catch { - const tempPersistent = path.join(os.tmpdir(), 'tldraw_repo_sync_data') - await fs.mkdir(tempPersistent, { recursive: true }) - return tempPersistent - } -} - -async function initBaseRepo(repoKey: keyof typeof repos, installationToken: string) { - const repo = repos[repoKey] - - const persistentDataPath = await getPersistentDataPath() - const repoPath = path.join(persistentDataPath, repo.path) - - try { - await fs.rm(repoPath, { recursive: true, force: true }) - } catch { - // dw - } - - const repoUrl = `https://x-access-token:${installationToken}@github.com/${repo.org}/${repo.name}.git` - await globalGitQueue.enqueue(() => exec('git', ['clone', '--mirror', repoUrl, repoPath])) -} - -export async function getBaseRepo(repoKey: keyof typeof repos, installationToken: string) { - const repo = repos[repoKey] - - return await repo.queue.enqueue(async () => { - const persistentDataPath = await getPersistentDataPath() - const repoPath = path.join(persistentDataPath, repo.path) - const git = createGit(repoPath) - - try { - await fs.readFile(path.join(repoPath, 'HEAD')) - } catch { - await initBaseRepo(repoKey, installationToken) - return { repo, path: repoPath } - } - - const remote = await git.trimmed('remote', 'get-url', 'origin') - if (!remote.endsWith(`@github.com/${repo.org}/${repo.name}.git`)) { - await initBaseRepo(repoKey, installationToken) - return { repo, path: repoPath } - } - - // update remote with a fresh JWT: - await git( - 'remote', - 'set-url', - 'origin', - `https://x-access-token:${installationToken}@github.com/${repo.org}/${repo.name}.git` - ) - - // make sure we're up to date with origin: - await git('remote', 'update') - - return { repo, path: repoPath } - }) -} - -export async function withWorkingRepo( - repoKey: keyof typeof repos, - installationToken: string, - ref: string, - fn: (opts: { repoPath: string; git: Git }) => Promise -) { - const { repo, path: repoPath } = await getBaseRepo(repoKey, installationToken) - const workingDir = path.join( - os.tmpdir(), - `tldraw_repo_sync_${Math.random().toString(36).slice(2)}` - ) - - await globalGitQueue.enqueue(() => exec('git', ['clone', '--no-checkout', repoPath, workingDir])) - const git = createGit(workingDir) - - try { - // update remote with a fresh JWT: - await git( - 'remote', - 'set-url', - 'origin', - `https://x-access-token:${installationToken}@github.com/${repo.org}/${repo.name}.git` - ) - - await git('checkout', ref) - await setLocalAuthorInfo(workingDir) - - return await fn({ repoPath: workingDir, git }) - } finally { - await fs.rm(workingDir, { recursive: true }) - } -} - -export async function setLocalAuthorInfo(pwd: string) { - const git = createGit(pwd) - await git('config', '--local', 'user.name', APP_USER_NAME) - await git('config', '--local', 'user.email', APP_USER_EMAIL) -} diff --git a/internal/huppy/src/reportError.tsx b/internal/huppy/src/reportError.tsx deleted file mode 100644 index 2b5eeed038f1..000000000000 --- a/internal/huppy/src/reportError.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import os from 'os' - -const discordWebhookUrl = process.env.HUPPY_WEBHOOK_URL - -export async function reportError(context: string, error: Error) { - if (typeof discordWebhookUrl === 'undefined') { - throw new Error('HUPPY_WEBHOOK_URL not set') - } - - const body = JSON.stringify({ - content: `[${os.hostname}] ${context}:\n\`\`\`\n${error.stack}\n\`\`\``, - }) - console.log(context, error.stack) - if (process.env.NODE_ENV !== 'production') return - await fetch(discordWebhookUrl, { - method: 'POST', - body, - headers: { - 'Content-Type': 'application/json', - }, - }) -} diff --git a/internal/huppy/src/requestWrapper.tsx b/internal/huppy/src/requestWrapper.tsx deleted file mode 100644 index 6b22b66b2df4..000000000000 --- a/internal/huppy/src/requestWrapper.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next' -import { reportError } from './reportError' - -export function wrapRequest( - name: string, - handler: (req: NextApiRequest, res: NextApiResponse) => Promise -) { - return async (req: NextApiRequest, res: NextApiResponse) => { - try { - await handler(req, res as NextApiResponse) - } catch (err: any) { - reportError(`Error in ${name}`, err) - res.status(500).json({ error: err.message }) - } - } -} diff --git a/internal/huppy/src/utils.ts b/internal/huppy/src/utils.ts deleted file mode 100644 index 3d086559a77e..000000000000 --- a/internal/huppy/src/utils.ts +++ /dev/null @@ -1,50 +0,0 @@ -import * as fs from 'fs/promises' -import json5 from 'json5' -import { NextApiRequest } from 'next' - -export function header(req: NextApiRequest, name: keyof NextApiRequest['headers']): string { - const value = req.headers[name] - if (!value) { - throw new Error(`Missing header: ${name}`) - } - if (Array.isArray(value)) { - throw new Error(`Header ${name} has multiple values`) - } - return value -} - -export function firstLine(str: string) { - return str.split('\n')[0] -} - -export function sleepMs(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)) -} - -export function camelCase(name: string) { - return name.replace(/[_-]([a-z0-9])/gi, (g) => g[1].toUpperCase()) -} - -export function capitalize(name: string) { - return name[0].toUpperCase() + name.slice(1) -} - -export function elapsed(start: number) { - return `${((Date.now() - start) / 1000).toFixed(2)}s` -} - -export async function readFileIfExists(file: string) { - try { - return await fs.readFile(file, 'utf8') - } catch { - return null - } -} - -export async function readJsonIfExists(file: string) { - const fileContents = await readFileIfExists(file) - if (fileContents === null) { - return null - } - return json5.parse(fileContents) -} diff --git a/internal/huppy/tsconfig.json b/internal/huppy/tsconfig.json deleted file mode 100644 index 31cc6e165ee6..000000000000 --- a/internal/huppy/tsconfig.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "compilerOptions": { - "target": "es5", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "node", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "downlevelIteration": true - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "_archive"], - "references": [ - { - "path": "../../packages/utils" - }, - { - "path": "../../packages/validate" - } - ] -} diff --git a/internal/scripts/publish-new.ts b/internal/scripts/publish-new.ts index 87452b302bcd..7beef4016d4a 100644 --- a/internal/scripts/publish-new.ts +++ b/internal/scripts/publish-new.ts @@ -1,5 +1,4 @@ import { Auto } from '@auto-it/core' -import fetch from 'cross-fetch' import glob from 'glob' import minimist from 'minimist' import { assert } from 'node:console' @@ -149,18 +148,6 @@ async function main() { // finally, publish the packages [IF THIS STEP FAILS, RUN THE `publish-manual.ts` script locally] await publish() - - nicelog('Notifying huppy of release...') - const huppyResponse = await fetch('https://tldraw-repo-sync.fly.dev/api/on-release', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ apiKey: huppyToken, tagToRelease: `v${nextVersion}`, canary: false }), - }) - nicelog( - `huppy: [${huppyResponse.status} ${huppyResponse.statusText}] ${await huppyResponse.text()}` - ) } main() diff --git a/internal/scripts/publish-patch.ts b/internal/scripts/publish-patch.ts index f6bc274ed7fb..0d47594fac2e 100644 --- a/internal/scripts/publish-patch.ts +++ b/internal/scripts/publish-patch.ts @@ -1,5 +1,4 @@ import { Auto } from '@auto-it/core' -import fetch from 'cross-fetch' import glob from 'glob' import { assert } from 'node:console' import { appendFileSync } from 'node:fs' @@ -116,20 +115,6 @@ async function main() { // semver rules will still be respected because there's no prerelease tag in the version, // so clients will get the updated version if they have a range like ^1.0.0 await publish(isLatestVersion ? 'latest' : 'revision') - - if (isLatestVersion) { - nicelog('Notifying huppy of release...') - const huppyResponse = await fetch('https://tldraw-repo-sync.fly.dev/api/on-release', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ apiKey: huppyToken, tagToRelease: `v${nextVersion}`, canary: false }), - }) - nicelog( - `huppy: [${huppyResponse.status} ${huppyResponse.statusText}] ${await huppyResponse.text()}` - ) - } } main() diff --git a/yarn.lock b/yarn.lock index 8b505add360b..e030db848491 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3799,7 +3799,7 @@ __metadata: languageName: node linkType: hard -"@octokit/plugin-retry@npm:^6.0.0, @octokit/plugin-retry@npm:^6.0.1": +"@octokit/plugin-retry@npm:^6.0.0": version: 6.0.1 resolution: "@octokit/plugin-retry@npm:6.0.1" dependencies: @@ -3940,13 +3940,6 @@ __metadata: languageName: node linkType: hard -"@octokit/webhooks-types@npm:^6.11.0": - version: 6.11.0 - resolution: "@octokit/webhooks-types@npm:6.11.0" - checksum: ad47a5a31291882177051b3dc201811000daa07d312157352c2cd23fd41541d0263ee326b78407788abc9dd04341cf395b6c4516f5ea6a98647e5cc33a3f709e - languageName: node - linkType: hard - "@octokit/webhooks@npm:^12.0.4": version: 12.0.11 resolution: "@octokit/webhooks@npm:12.0.11" @@ -7908,7 +7901,7 @@ __metadata: languageName: node linkType: hard -"@types/jsonwebtoken@npm:^9.0.0, @types/jsonwebtoken@npm:^9.0.1": +"@types/jsonwebtoken@npm:^9.0.0": version: 9.0.5 resolution: "@types/jsonwebtoken@npm:9.0.5" dependencies: @@ -14927,28 +14920,6 @@ __metadata: languageName: node linkType: hard -"huppy@workspace:internal/huppy": - version: 0.0.0-use.local - resolution: "huppy@workspace:internal/huppy" - dependencies: - "@octokit/core": "npm:^5.0.1" - "@octokit/plugin-retry": "npm:^6.0.1" - "@octokit/webhooks-types": "npm:^6.11.0" - "@tldraw/utils": "workspace:*" - "@tldraw/validate": "workspace:*" - "@types/jsonwebtoken": "npm:^9.0.1" - eslint: "npm:^9.13.0" - eslint-config-next: "npm:^15.0.0" - json5: "npm:^2.2.3" - jsonwebtoken: "npm:^9.0.0" - lazyrepo: "npm:0.0.0-alpha.27" - next: "npm:^14.0.4" - octokit: "npm:^3.1.1" - react: "npm:^18.2.0" - react-dom: "npm:^18.2.0" - languageName: unknown - linkType: soft - "husky@npm:^8.0.0": version: 8.0.3 resolution: "husky@npm:8.0.3" @@ -16731,7 +16702,7 @@ __metadata: languageName: node linkType: hard -"jsonwebtoken@npm:^9.0.0, jsonwebtoken@npm:^9.0.2": +"jsonwebtoken@npm:^9.0.2": version: 9.0.2 resolution: "jsonwebtoken@npm:9.0.2" dependencies: