diff --git a/.github/workflows/bot.yml b/.github/workflows/bot.yml
new file mode 100644
index 00000000..0495d9a9
--- /dev/null
+++ b/.github/workflows/bot.yml
@@ -0,0 +1,287 @@
+name: Test, Build & Deploy the Comfy-PR Bot to Cloud Run Service
+
+on:
+ push:
+ branches: [main, dev]
+ pull_request:
+ branches: [main]
+
+concurrency:
+ group: deploy-${{ github.head_ref || github.ref }}
+ cancel-in-progress: false
+
+env:
+ PROJECT_ID: dreamboothy-dev
+ GAR_LOCATION: us-west2
+ SERVICE: comfy-pr
+ REGION: us-west2
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ if: github.event_name == 'pull_request' || github.ref == 'refs/heads/main'
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v5
+
+ # Build and test
+ - name: Setup Bun
+ uses: oven-sh/setup-bun@v2
+
+ - name: Install dependencies
+ run: bun install --frozen-lockfile
+
+ - name: Cache TypeScript incremental build
+ uses: actions/cache@v3
+ with:
+ path: tsconfig.tsbuildinfo
+ key: ${{ runner.os }}-tsc-${{ hashFiles('**/*.ts', '**/*.tsx', 'tsconfig.json') }}
+ restore-keys: |
+ ${{ runner.os }}-tsc-
+
+ - name: Type check
+ run: bunx tsc --noEmit
+
+ - name: Run tests
+ run: bun test
+
+ - name: Build Next.js app
+ run: bun run build
+
+ # Deploy to CloudRun
+ - name: Google Auth
+ id: auth
+ uses: google-github-actions/auth@v2
+ with:
+ credentials_json: "${{ secrets.GCP_SA_KEY }}"
+
+ - name: Cache Google Cloud SDK
+ uses: actions/cache@v3
+ with:
+ path: |
+ ~/.config/gcloud
+ ~/google-cloud-sdk
+ key: ${{ runner.os }}-gcloud-${{ hashFiles('**/deploy.yml') }}
+ restore-keys: |
+ ${{ runner.os }}-gcloud-
+
+ - name: Set up Cloud SDK (gcloud)
+ uses: google-github-actions/setup-gcloud@v2
+ with:
+ project_id: ${{ env.PROJECT_ID }}
+ skip_install: false
+ install_components: ""
+
+ - name: Configure Docker to use gcloud
+ run: gcloud auth configure-docker $GAR_LOCATION-docker.pkg.dev --quiet
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Build and Push Container
+ run: |-
+ # Get branch name for tagging
+ BRANCH_NAME=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}
+ SANITIZED_BRANCH=$(echo "$BRANCH_NAME" | sed 's|/|-|g' | sed 's|_|-|g' | tr '[:upper:]' '[:lower:]')
+
+ # Build with multiple tags: SHA, branch, and latest
+ docker buildx build \
+ --cache-from=type=registry,ref=$GAR_LOCATION-docker.pkg.dev/$PROJECT_ID/$SERVICE/$SERVICE:buildcache \
+ --cache-from=type=registry,ref=$GAR_LOCATION-docker.pkg.dev/$PROJECT_ID/$SERVICE/$SERVICE:latest \
+ --cache-to=type=registry,ref=$GAR_LOCATION-docker.pkg.dev/$PROJECT_ID/$SERVICE/$SERVICE:buildcache,mode=max \
+ --push \
+ --build-arg BUILDKIT_INLINE_CACHE=1 \
+ -t "$GAR_LOCATION-docker.pkg.dev/$PROJECT_ID/$SERVICE/$SERVICE:$GITHUB_SHA" \
+ -t "$GAR_LOCATION-docker.pkg.dev/$PROJECT_ID/$SERVICE/$SERVICE:branch-$SANITIZED_BRANCH" \
+ -t "$GAR_LOCATION-docker.pkg.dev/$PROJECT_ID/$SERVICE/$SERVICE:latest" \
+ .
+
+ - name: Extract branch name
+ id: extract_branch
+ run: |
+ BRANCH_NAME=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}
+
+ # For PRs, use the actual branch SHA instead of the merge commit SHA
+ if [[ "${{ github.event_name }}" == "pull_request" ]]; then
+ ACTUAL_SHA="${{ github.event.pull_request.head.sha }}"
+ echo "Using PR head SHA: $ACTUAL_SHA (instead of merge commit: $GITHUB_SHA)"
+ else
+ ACTUAL_SHA="$GITHUB_SHA"
+ fi
+
+ # Sanitize branch name for Cloud Run tags
+ SANITIZED_BRANCH=$(echo "$BRANCH_NAME" | sed 's|/|-|g' | sed 's|_|-|g' | tr '[:upper:]' '[:lower:]' | sed 's/-*$//')
+ # If branch starts with a number, prefix with 'br-'
+ if [[ "$SANITIZED_BRANCH" =~ ^[0-9] ]]; then
+ SANITIZED_BRANCH="br-${SANITIZED_BRANCH}"
+ fi
+
+ # Create revision suffix: branch-first7chars (max 30 chars for branch, 7 for hash)
+ BRANCH_SUFFIX=$(echo "$SANITIZED_BRANCH" | cut -c1-30)
+ COMMIT_SHORT=$(echo "$ACTUAL_SHA" | cut -c1-7)
+ REVISION_SUFFIX="${BRANCH_SUFFIX}-${COMMIT_SHORT}"
+
+ # Truncate tag to fit within Cloud Run's 46 char limit
+ TAG_FOR_TRAFFIC=$(echo "$SANITIZED_BRANCH" | cut -c1-37)
+
+ echo "branch=$BRANCH_NAME" >> $GITHUB_OUTPUT
+ echo "sanitized_branch=$SANITIZED_BRANCH" >> $GITHUB_OUTPUT
+ echo "revision_suffix=$REVISION_SUFFIX" >> $GITHUB_OUTPUT
+ echo "tag_for_traffic=$TAG_FOR_TRAFFIC" >> $GITHUB_OUTPUT
+ echo "Deploying branch: $BRANCH_NAME (sanitized: $SANITIZED_BRANCH)"
+ echo "Revision suffix: $REVISION_SUFFIX"
+ echo "Traffic tag: $TAG_FOR_TRAFFIC"
+
+ - name: Deploy to Cloud Run
+ id: deploy
+ uses: google-github-actions/deploy-cloudrun@v2
+ with:
+ service: ${{ env.SERVICE }}
+ region: ${{ env.REGION }}
+ image: ${{ env.GAR_LOCATION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.SERVICE }}/${{
+ env.SERVICE }}:${{ github.sha }}
+ flags: |
+ ${{ steps.extract_branch.outputs.branch == 'main' && '--memory=2Gi --cpu=2' || '--memory=1Gi --cpu=1' }}
+ --port=80
+ --allow-unauthenticated
+ --service-account=comfy-pr-sa@${{ env.PROJECT_ID }}.iam.gserviceaccount.com
+ --update-labels=branch=${{ steps.extract_branch.outputs.sanitized_branch }},commit=${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }},deployed-by=github-actions
+ --tag=${{ steps.extract_branch.outputs.tag_for_traffic }}
+ --revision-suffix=${{ steps.extract_branch.outputs.revision_suffix }}
+ ${{ steps.extract_branch.outputs.branch == 'main' && '--min-instances=1' || '--min-instances=0' }}
+ --max-instances=1
+ ${{ steps.extract_branch.outputs.branch != 'main' && '--no-traffic' || '' }}
+ env_vars: |
+ BRANCH_NAME=${{ steps.extract_branch.outputs.branch }}
+ NODE_ENV=production
+ GH_TOKEN=${{ secrets.GH_TOKEN }}
+ SALT=${{ secrets.SALT }}
+ GIT_USEREMAIL=${{ secrets.GIT_USEREMAIL }}
+ GIT_USERNAME=${{ secrets.GIT_USERNAME }}
+ FORK_PREFIX=${{ secrets.FORK_PREFIX }}
+ FORK_OWNER=${{ secrets.FORK_OWNER }}
+ MONGODB_URI=${{ secrets.MONGODB_URI }}
+ SLACK_BOT_TOKEN=${{ secrets.SLACK_BOT_TOKEN }}
+ NOTION_TOKEN=${{ secrets.NOTION_TOKEN }}
+
+ - name: Generate tagged URL
+ id: tagged_url
+ if: steps.extract_branch.outputs.branch != 'main'
+ run: |
+ # Generate the correct tagged URL format: https://{tag}---{service}-{hash}.a.run.app
+ SERVICE_URL="${{ steps.deploy.outputs.url }}"
+ TAGGED_URL=$(echo "$SERVICE_URL" | sed "s|https://|https://${{ steps.extract_branch.outputs.tag_for_traffic }}---|")
+ echo "tagged_url=$TAGGED_URL" >> $GITHUB_OUTPUT
+ echo "Generated tagged URL: $TAGGED_URL"
+
+ - name: Comment on PR
+ if: github.event_name == 'pull_request'
+ continue-on-error: true
+ uses: edumserrano/find-create-or-update-comment@v3
+ with:
+ issue-number: ${{ github.event.pull_request.number }}
+ body-includes: ""
+ comment-author: "github-actions[bot]"
+ edit-mode: replace
+ body: |
+
+ ## đ Deployment Ready
+
+ Your PR has been deployed successfully!
+
+ **đ Cloud Run URL:** ${{ steps.tagged_url.outputs.tagged_url }}
+
+ **Branch:** `${{ steps.extract_branch.outputs.branch }}`
+ **Commit:** `${{ github.sha }}`
+ **Revision:** `${{ env.SERVICE }}-${{ steps.extract_branch.outputs.revision_suffix }}`
+ **Scaling:** Min instances: 0, Max instances: 1 (scales to zero when not in use)
+
+ ---
+
+ đ **[View Logs in GCP Console](https://console.cloud.google.com/logs/query;query=resource.type%3D%22cloud_run_revision%22%0Aresource.labels.service_name%3D%22${{ env.SERVICE }}%22%0Aresource.labels.revision_name%3D%22${{ env.SERVICE }}-${{ steps.extract_branch.outputs.revision_suffix }}%22;timeRange=PT1H?project=${{ env.PROJECT_ID }})**
+
+ ---
+
+ âšī¸ Note: This deployment receives no production traffic and will be automatically cleaned up when the PR is closed.
+
+
+ đ Deployment Details
+
+ - **Service:** `${{ env.SERVICE }}`
+ - **Region:** `${{ env.REGION }}`
+ - **Image:** `${{ env.GAR_LOCATION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.SERVICE }}/${{ env.SERVICE }}:${{ github.sha }}`
+ - **Labels:** `branch=${{ steps.extract_branch.outputs.branch }}, commit=${{ github.sha }}, deployed-by=github-actions`
+
+
+
+ - name: Health Check Cloud Run
+ id: healthcheck
+ run: |
+ # Check the Cloud Run backend directly
+ if [ "${{ steps.extract_branch.outputs.branch }}" == "main" ]; then
+ HEALTH_URL="${{ steps.deploy.outputs.url }}/api/health"
+ echo "đĨ Starting health check for main branch: $HEALTH_URL"
+ else
+ # For PR branches, check the tagged URL directly
+ HEALTH_URL="${{ steps.tagged_url.outputs.tagged_url }}/api/health"
+ echo "đĨ Starting health check for Cloud Run: $HEALTH_URL"
+ fi
+
+ # Health check with timeout (3 minutes = 180 seconds)
+ MAX_ATTEMPTS=36 # 36 attempts * 5 seconds = 180 seconds
+ ATTEMPT=1
+
+ while [ $ATTEMPT -le $MAX_ATTEMPTS ]; do
+ echo "Attempt $ATTEMPT/$MAX_ATTEMPTS..."
+
+ # Make health check request
+ HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$HEALTH_URL" || echo "000")
+
+ if [ "$HTTP_STATUS" == "200" ]; then
+ echo "â
Health check passed! Server is up and running."
+ HEALTH_RESPONSE=$(curl -s "$HEALTH_URL")
+ echo "Health response: $HEALTH_RESPONSE"
+ echo "health_status=success" >> $GITHUB_OUTPUT
+ exit 0
+ else
+ echo "âŗ Health check returned HTTP $HTTP_STATUS, retrying in 5 seconds..."
+ fi
+
+ ATTEMPT=$((ATTEMPT + 1))
+ [ $ATTEMPT -le $MAX_ATTEMPTS ] && sleep 5
+ done
+
+ echo "â Health check failed after 3 minutes. Server did not respond with HTTP 200."
+ echo "health_status=failed" >> $GITHUB_OUTPUT
+ exit 1
+
+ - name: Set Traffic to 100% for Main Branch
+ if: steps.extract_branch.outputs.branch == 'main' && steps.healthcheck.outputs.health_status
+ == 'success'
+ run: |
+ echo "đĻ Setting traffic to 100% for main branch deployment..."
+
+ # Update traffic to route 100% to the specific revision that was just deployed
+ gcloud run services update-traffic ${{ env.SERVICE }} \
+ --region=${{ env.REGION }} \
+ --to-revisions=${{ env.SERVICE }}-${{ steps.extract_branch.outputs.revision_suffix }}=100 \
+ --platform=managed
+
+ echo "â
Traffic routing updated: 100% traffic now directed to revision ${{ env.SERVICE }}-${{ steps.extract_branch.outputs.revision_suffix }}"
+
+ - name: Show Output
+ run: |
+ echo "đ Deployment completed!"
+ echo "Cloud Run Service URL: ${{ steps.deploy.outputs.url }}"
+ if [ "${{ steps.healthcheck.outputs.health_status }}" == "success" ]; then
+ echo "â
Backend health check passed"
+ if [ "${{ github.event_name }}" == "pull_request" ]; then
+ echo "đ Cloud Run Tagged URL: ${{ steps.tagged_url.outputs.tagged_url }}"
+ echo "âšī¸ This PR deployment receives no production traffic (--no-traffic)"
+ else
+ echo "â
Main branch deployment receiving production traffic"
+ fi
+ else
+ echo "â ī¸ Backend health check failed"
+ echo "Please check Cloud Run logs for details"
+ fi
diff --git a/.github/workflows/easylabel.yaml b/.github/workflows/easylabel.yaml
index 76391fc2..f885d76d 100644
--- a/.github/workflows/easylabel.yaml
+++ b/.github/workflows/easylabel.yaml
@@ -21,7 +21,7 @@ jobs:
- run: bun i
# Run EasyLabel task with timeout
- - run: bun run/easylabel.tsx
+ - run: bun bot/easylabel.tsx
env:
GH_TOKEN_COMFY_PR: ${{ secrets.GH_TOKEN_COMFY_PR_BOT }}
MONGODB_URI: ${{ secrets.MONGODB_URI }}
diff --git a/.gitignore b/.gitignore
index 1961b60d..71d2d422 100644
--- a/.gitignore
+++ b/.gitignore
@@ -77,3 +77,11 @@ codes/
tmp/
TODO.md
+
+# Terraform
+infra/.terraform/
+infra/.terraform.lock.hcl
+infra/*.tfstate
+infra/*.tfstate.*
+infra/*.tfvars
+infra/*.tfplan
diff --git a/CLAUDE.md b/CLAUDE.md
index 0c5cd0b1..5daee5bf 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -57,7 +57,7 @@ The TypeScript server was experiencing severe performance issues causing slowdow
- **`app/tasks/`**: Specific task implementations
- **`bot/code/`**: GitHub integration tools including prbot for spawning AI agents on repositories
- **`gh-service/`**: GitHub webhook service components
-- **`run/`**: Executable scripts and services
+- **`bot/`**: Executable scripts and services
- **Tests**: Co-located with source files using `.spec.ts` suffix
## Working Tasks State Management
@@ -735,6 +735,7 @@ SFlow is a powerful functional stream processing library used throughout the cod
- **Author**: snomiao
- **License**: MIT
- **Core Concepts**: SFlow is built around composable stream operators, lazy evaluation, and support for both synchronous and asynchronous data flows.
+<<<<<<< HEAD
## Project TODOs
diff --git a/README.md b/README.md
index f9258003..110443e4 100644
--- a/README.md
+++ b/README.md
@@ -32,7 +32,7 @@ Through these efforts, Comfy-PR seeks to create an environment where Custom Node
1. **CLI Tool** (`src/cli.ts`): Command-line interface for processing individual repositories
2. **Main Service** (`src/index.ts`): Orchestrates batch processing of repositories
3. **Web Dashboard** (`app/`): Next.js application with analytics and management UI
-4. **Webhook Service** (`run/index.ts`): Real-time GitHub event monitoring
+4. **Webhook Service** (`bot/index.ts`): Real-time GitHub event monitoring
5. **Task System** (`app/tasks/`): Automated background tasks for various operations
### Key Features
@@ -240,7 +240,7 @@ Comfy-PR/
â âââ (dashboard)/ # Dashboard pages and components
â âââ api/ # API routes and tRPC
â âââ tasks/ # Background task implementations
-âââ run/ # Production services
+âââ bot/ # Production services
â âââ index.ts # GitHub webhook service
â âââ deploy.sh # Cloud deployment scripts
âââ gh-service/ # Legacy webhook service
@@ -308,7 +308,7 @@ export WEBHOOK_BASE_URL=https://your-domain.com
bun run gh-service
```
-See [WEBHOOK_SETUP.md](./WEBHOOK_SETUP.md) for detailed webhook configuration.
+See [WEBHOOK_SETUP.md](./docs/WEBHOOK_SETUP.md) for detailed webhook configuration.
### Task System
diff --git a/app/(dashboard)/cnrepos/page.tsx b/app/(dashboard)/cnrepos/page.tsx
index 13d6f433..49188864 100644
--- a/app/(dashboard)/cnrepos/page.tsx
+++ b/app/(dashboard)/cnrepos/page.tsx
@@ -11,6 +11,9 @@ import { CNRepos } from "@/src/CNRepos";
import { Suspense } from "react";
import { CNReposTableClient } from "./CNReposTableClient";
+// Prevent static generation since this page requires database access
+export const dynamic = "force-dynamic";
+
interface CNReposPageProps {
searchParams?: Promise<{
page?: string;
diff --git a/app/(dashboard)/page.tsx b/app/(dashboard)/page.tsx
index f32c4e7b..bebae37b 100644
--- a/app/(dashboard)/page.tsx
+++ b/app/(dashboard)/page.tsx
@@ -3,7 +3,7 @@ import { Suspense } from "react";
import DetailsTable from "./DetailsTable";
import TotalsPage from "./totals/page";
-// Force dynamic rendering to avoid build-time database access
+// Prevent static generation since this page requires database access
export const dynamic = "force-dynamic";
export const revalidate = 60; // seconds
diff --git a/app/(dashboard)/repos/page.tsx b/app/(dashboard)/repos/page.tsx
index 0cf8fa77..ffc1d7c6 100644
--- a/app/(dashboard)/repos/page.tsx
+++ b/app/(dashboard)/repos/page.tsx
@@ -2,7 +2,7 @@ import { CNRepos } from "@/src/CNRepos";
import { Suspense } from "react";
import yaml from "yaml";
-// Force dynamic rendering to avoid build-time database access
+// Prevent static generation since this page requires database access
export const dynamic = "force-dynamic";
/**
diff --git a/app/bot/page.tsx b/app/bot/page.tsx
new file mode 100644
index 00000000..9bca1731
--- /dev/null
+++ b/app/bot/page.tsx
@@ -0,0 +1,3 @@
+"use client";
+
+export default function BotPage() {}
diff --git a/app/tasks/gh-core-tag-notification/index.spec.ts b/app/tasks/gh-core-tag-notification/index.spec.ts
index 9574bff4..9309261b 100644
--- a/app/tasks/gh-core-tag-notification/index.spec.ts
+++ b/app/tasks/gh-core-tag-notification/index.spec.ts
@@ -1,49 +1,42 @@
-import { gh } from "@/lib/github";
-import { getSlackChannel } from "@/lib/slack/channels";
-import { afterEach, beforeEach, describe, expect, it, jest } from "bun:test";
+import { gh } from "@/src/gh";
+import { getSlackChannel } from "@/src/slack/channels";
+import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test";
import { upsertSlackMessage } from "../gh-desktop-release-notification/upsertSlackMessage";
-jest.mock("@/src/gh");
-jest.mock("@/src/slack/channels");
-jest.mock("../gh-desktop-release-notification/upsertSlackMessage");
+mock.module("@/src/gh", () => ({ gh: {} }));
+mock.module("@/src/slack/channels", () => ({ getSlackChannel: mock() }));
+mock.module("../gh-desktop-release-notification/upsertSlackMessage", () => ({ upsertSlackMessage: mock() }));
const mockCollection = {
- createIndex: jest.fn().mockResolvedValue({}),
- findOne: jest.fn().mockResolvedValue(null),
- findOneAndUpdate: jest.fn().mockImplementation((_filter, update) => Promise.resolve(update.$set)),
+ createIndex: mock(() => Promise.resolve({})),
+ findOne: mock(() => Promise.resolve(null)) as any,
+ findOneAndUpdate: mock((_filter: any, update: any) => Promise.resolve(update.$set)) as any,
};
-jest.mock("@/src/db", () => ({
+mock.module("@/src/db", () => ({
db: {
- collection: jest.fn(() => mockCollection),
+ collection: mock(() => mockCollection),
},
}));
import runGithubCoreTagNotificationTask from "./index";
describe("GithubCoreTagNotificationTask", () => {
- const mockGh = gh as jest.Mocked;
- const mockGetSlackChannel = getSlackChannel as jest.MockedFunction;
- const mockUpsertSlackMessage = upsertSlackMessage as jest.MockedFunction<
- typeof upsertSlackMessage
- >;
+ const mockGh = gh as any;
+ const mockGetSlackChannel = getSlackChannel as any;
+ const mockUpsertSlackMessage = upsertSlackMessage as any;
beforeEach(() => {
- jest.clearAllMocks();
- mockCollection.findOne.mockResolvedValue(null);
- mockCollection.findOneAndUpdate.mockImplementation((_filter, update) =>
- Promise.resolve(update.$set),
- );
- mockGetSlackChannel.mockImplementation((channelName: string) =>
- Promise.resolve({
- id: channelName === "desktop" ? "test-channel-desktop" : "test-channel-live-ops",
- name: channelName,
- } as any),
- );
+ mockCollection.findOne.mockClear();
+ mockCollection.findOneAndUpdate.mockClear();
+ mockCollection.findOne.mockImplementation(() => Promise.resolve(null));
+ mockCollection.findOneAndUpdate.mockImplementation((_filter: any, update: any) => Promise.resolve(update.$set));
+ mockGetSlackChannel.mockResolvedValue({ id: "test-channel-id", name: "desktop" } as any);
});
afterEach(() => {
- jest.clearAllMocks();
+ mockCollection.findOne.mockClear();
+ mockCollection.findOneAndUpdate.mockClear();
});
it("should fetch tags from the ComfyUI repository", async () => {
@@ -61,19 +54,21 @@ describe("GithubCoreTagNotificationTask", () => {
];
mockGh.repos = {
- listTags: jest.fn().mockResolvedValue({ data: mockTags }),
- getCommit: jest.fn().mockResolvedValue({
- data: {
- commit: {
- author: { date: new Date().toISOString() },
- committer: { date: new Date().toISOString() },
+ listTags: mock(() => Promise.resolve({ data: mockTags })),
+ getCommit: mock(() =>
+ Promise.resolve({
+ data: {
+ commit: {
+ author: { date: new Date().toISOString() },
+ committer: { date: new Date().toISOString() },
+ },
},
- },
- }),
+ }),
+ ),
} as any;
mockGh.git = {
- getTag: jest.fn().mockRejectedValue(new Error("Not an annotated tag")),
+ getTag: mock(() => Promise.reject(new Error("Not an annotated tag"))),
} as any;
mockUpsertSlackMessage.mockResolvedValue({
@@ -103,28 +98,32 @@ describe("GithubCoreTagNotificationTask", () => {
];
mockGh.repos = {
- listTags: jest.fn().mockResolvedValue({ data: mockTags }),
- getCommit: jest.fn().mockResolvedValue({
- data: {
- commit: {
- author: { date: new Date().toISOString() },
+ listTags: mock(() => Promise.resolve({ data: mockTags })),
+ getCommit: mock(() =>
+ Promise.resolve({
+ data: {
+ commit: {
+ author: { date: new Date().toISOString() },
+ },
},
- },
- }),
+ }),
+ ),
} as any;
mockGh.git = {
- getTag: jest.fn().mockResolvedValue({
- data: {
- tag: "v0.2.2",
- tagger: {
- date: new Date().toISOString(),
- name: "Test Author",
- email: "test@example.com",
+ getTag: mock(() =>
+ Promise.resolve({
+ data: {
+ tag: "v0.2.2",
+ tagger: {
+ date: new Date().toISOString(),
+ name: "Test Author",
+ email: "test@example.com",
+ },
+ message: "Release v0.2.2 with new features",
},
- message: "Release v0.2.2 with new features",
- },
- }),
+ }),
+ ),
} as any;
mockUpsertSlackMessage.mockResolvedValue({
@@ -150,18 +149,20 @@ describe("GithubCoreTagNotificationTask", () => {
];
mockGh.repos = {
- listTags: jest.fn().mockResolvedValue({ data: mockTags }),
- getCommit: jest.fn().mockResolvedValue({
- data: {
- commit: {
- author: { date: new Date().toISOString() },
+ listTags: mock(() => Promise.resolve({ data: mockTags })),
+ getCommit: mock(() =>
+ Promise.resolve({
+ data: {
+ commit: {
+ author: { date: new Date().toISOString() },
+ },
},
- },
- }),
+ }),
+ ),
} as any;
mockGh.git = {
- getTag: jest.fn().mockRejectedValue(new Error("Not an annotated tag")),
+ getTag: mock(() => Promise.reject(new Error("Not an annotated tag"))),
} as any;
mockUpsertSlackMessage.mockResolvedValue({
@@ -200,26 +201,21 @@ describe("GithubCoreTagNotificationTask", () => {
];
mockGh.repos = {
- listTags: jest.fn().mockResolvedValue({ data: mockTags }),
+ listTags: mock(() => Promise.resolve({ data: mockTags })),
} as any;
- mockCollection.findOne.mockResolvedValue({
- tagName: "v0.2.0",
- commitSha: "existing123",
- url: "https://github.com/comfyanonymous/ComfyUI/releases/tag/v0.2.0",
- slackMessages: [
- {
- text: "Already sent",
- channel: "test-channel-desktop",
- url: "https://slack.com/message/old1",
- },
- {
+ mockCollection.findOne.mockImplementation(() =>
+ Promise.resolve({
+ tagName: "v0.2.0",
+ commitSha: "existing123",
+ url: "https://github.com/comfyanonymous/ComfyUI/releases/tag/v0.2.0",
+ slackMessage: {
text: "Already sent",
- channel: "test-channel-live-ops",
- url: "https://slack.com/message/old2",
+ channel: "test-channel-id",
+ url: "https://slack.com/message/old",
},
- ],
- });
+ }),
+ );
await runGithubCoreTagNotificationTask();
@@ -240,21 +236,23 @@ describe("GithubCoreTagNotificationTask", () => {
const tagMessage = "Major release with breaking changes";
mockGh.repos = {
- listTags: jest.fn().mockResolvedValue({ data: mockTags }),
+ listTags: mock(() => Promise.resolve({ data: mockTags })),
} as any;
mockGh.git = {
- getTag: jest.fn().mockResolvedValue({
- data: {
- tag: "v0.3.0",
- tagger: {
- date: new Date().toISOString(),
- name: "Test Author",
- email: "test@example.com",
+ getTag: mock(() =>
+ Promise.resolve({
+ data: {
+ tag: "v0.3.0",
+ tagger: {
+ date: new Date().toISOString(),
+ name: "Test Author",
+ email: "test@example.com",
+ },
+ message: tagMessage,
},
- message: tagMessage,
- },
- }),
+ }),
+ ),
} as any;
mockUpsertSlackMessage.mockResolvedValue({
@@ -285,25 +283,29 @@ describe("GithubCoreTagNotificationTask", () => {
];
mockGh.repos = {
- listTags: jest.fn().mockResolvedValue({ data: mockTags }),
- getCommit: jest.fn().mockResolvedValue({
- data: {
- commit: {
- author: { date: oldDate.toISOString() },
+ listTags: mock(() => Promise.resolve({ data: mockTags })),
+ getCommit: mock(() =>
+ Promise.resolve({
+ data: {
+ commit: {
+ author: { date: oldDate.toISOString() },
+ },
},
- },
- }),
+ }),
+ ),
} as any;
mockGh.git = {
- getTag: jest.fn().mockResolvedValue({
- data: {
- tag: "v0.1.0",
- tagger: {
- date: oldDate.toISOString(),
+ getTag: mock(() =>
+ Promise.resolve({
+ data: {
+ tag: "v0.1.0",
+ tagger: {
+ date: oldDate.toISOString(),
+ },
},
- },
- }),
+ }),
+ ),
} as any;
await runGithubCoreTagNotificationTask();
diff --git a/app/tasks/gh-design/page.tsx b/app/tasks/gh-design/page.tsx
index 320e0f6b..d7b3495e 100644
--- a/app/tasks/gh-design/page.tsx
+++ b/app/tasks/gh-design/page.tsx
@@ -1,17 +1,9 @@
import { Badge } from "@/components/ui/badge";
-import {
- Table,
- TableBody,
- TableCaption,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from "@/components/ui/table";
+import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import Link from "next/link";
import { GithubDesignTaskMetaEditor } from "./GithubDesignTaskMetaEditor";
-// Force dynamic rendering to avoid build-time database access
+// Prevent static generation since this page requires database access
export const dynamic = "force-dynamic";
/**
@@ -114,9 +106,13 @@ export default async function GithubDesignTaskPage() {
{{ pull_request: "PR", issue: "Issue" }[task.type] || "Task"}
+<<<<<<< HEAD
{getIssueNumber(task.url)}
+=======
+ {getIssueNumber(task.url)}
+>>>>>>> 6c13502 (fix: mark gh-design task page as dynamic)
{task.title}
diff --git a/app/tasks/gh-desktop-release-notification/index.spec.ts b/app/tasks/gh-desktop-release-notification/index.spec.ts
index 29db989e..e1119e2c 100644
--- a/app/tasks/gh-desktop-release-notification/index.spec.ts
+++ b/app/tasks/gh-desktop-release-notification/index.spec.ts
@@ -1,39 +1,36 @@
-import { gh } from "@/lib/github";
-import { getSlackChannel } from "@/lib/slack/channels";
-import { afterEach, beforeEach, describe, expect, it, jest } from "bun:test";
+import { gh } from "@/src/gh";
+import { getSlackChannel } from "@/src/slack/channels";
+import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test";
import { upsertSlackMessage } from "./upsertSlackMessage";
-jest.mock("@/src/gh");
-jest.mock("@/src/slack/channels");
-jest.mock("./upsertSlackMessage");
+mock.module("@/src/gh", () => ({ gh: {} }));
+mock.module("@/src/slack/channels", () => ({ getSlackChannel: mock() }));
+mock.module("./upsertSlackMessage", () => ({ upsertSlackMessage: mock() }));
const mockCollection = {
- createIndex: jest.fn().mockResolvedValue({}),
- findOne: jest.fn().mockResolvedValue(null),
- findOneAndUpdate: jest.fn().mockImplementation((_filter, update) => Promise.resolve(update.$set)),
+ createIndex: mock(() => Promise.resolve({})),
+ findOne: mock(() => Promise.resolve(null)) as any,
+ findOneAndUpdate: mock((_filter: any, update: any) => Promise.resolve(update.$set)) as any,
};
-jest.mock("@/src/db", () => ({
+mock.module("@/src/db", () => ({
db: {
- collection: jest.fn(() => mockCollection),
+ collection: mock(() => mockCollection),
},
}));
import runGithubDesktopReleaseNotificationTask from "./index";
describe("GithubDesktopReleaseNotificationTask", () => {
- const mockGh = gh as jest.Mocked;
- const mockGetSlackChannel = getSlackChannel as jest.MockedFunction;
- const mockUpsertSlackMessage = upsertSlackMessage as jest.MockedFunction<
- typeof upsertSlackMessage
- >;
+ const mockGh = gh as any;
+ const mockGetSlackChannel = getSlackChannel as any;
+ const mockUpsertSlackMessage = upsertSlackMessage as any;
beforeEach(async () => {
- jest.clearAllMocks();
- mockCollection.findOne.mockResolvedValue(null);
- mockCollection.findOneAndUpdate.mockImplementation((_filter, update) =>
- Promise.resolve(update.$set),
- );
+ mockCollection.findOne.mockClear();
+ mockCollection.findOneAndUpdate.mockClear();
+ mockCollection.findOne.mockImplementation(() => Promise.resolve(null));
+ mockCollection.findOneAndUpdate.mockImplementation((_filter: any, update: any) => Promise.resolve(update.$set));
mockGetSlackChannel.mockResolvedValue({
id: "test-channel-id",
@@ -48,7 +45,8 @@ describe("GithubDesktopReleaseNotificationTask", () => {
});
afterEach(async () => {
- jest.clearAllMocks();
+ mockCollection.findOne.mockClear();
+ mockCollection.findOneAndUpdate.mockClear();
});
describe("Draft Release Processing - Bug Fix Verification", () => {
@@ -64,9 +62,9 @@ describe("GithubDesktopReleaseNotificationTask", () => {
};
mockGh.repos = {
- listReleases: jest.fn().mockResolvedValue({
+ listReleases: mock(() => Promise.resolve({
data: [mockDraftRelease],
- }),
+ })),
} as any;
// First call - save initial draft data
@@ -138,9 +136,9 @@ describe("GithubDesktopReleaseNotificationTask", () => {
};
mockGh.repos = {
- listReleases: jest.fn().mockResolvedValue({
+ listReleases: mock(() => Promise.resolve({
data: [mockDraftRelease],
- }),
+ })),
} as any;
const expectedText =
@@ -184,9 +182,9 @@ describe("GithubDesktopReleaseNotificationTask", () => {
};
mockGh.repos = {
- listReleases: jest.fn().mockResolvedValue({
+ listReleases: mock(() => Promise.resolve({
data: [mockDraftRelease],
- }),
+ })),
} as any;
// Return task with old drafting message text
@@ -243,9 +241,9 @@ describe("GithubDesktopReleaseNotificationTask", () => {
};
mockGh.repos = {
- listReleases: jest.fn().mockResolvedValue({
+ listReleases: mock(() => Promise.resolve({
data: [mockStableRelease],
- }),
+ })),
} as any;
// First call - save initial data
@@ -306,9 +304,9 @@ describe("GithubDesktopReleaseNotificationTask", () => {
};
mockGh.repos = {
- listReleases: jest.fn().mockResolvedValue({
+ listReleases: mock(() => Promise.resolve({
data: [mockStableRelease],
- }),
+ })),
} as any;
const expectedText =
@@ -352,9 +350,9 @@ describe("GithubDesktopReleaseNotificationTask", () => {
};
mockGh.repos = {
- listReleases: jest.fn().mockResolvedValue({
+ listReleases: mock(() => Promise.resolve({
data: [mockPrerelease],
- }),
+ })),
} as any;
// First call - save initial data
@@ -417,7 +415,7 @@ describe("GithubDesktopReleaseNotificationTask", () => {
};
mockGh.repos = {
- listReleases: jest.fn().mockResolvedValue({
+ listReleases: mock(() => Promise.resolve({
data: [mockDesktopRelease],
}),
} as any;
@@ -434,13 +432,15 @@ describe("GithubDesktopReleaseNotificationTask", () => {
});
// Find core task
- mockCollection.findOne.mockResolvedValueOnce({
- version: "v0.2.0",
- slackMessage: {
- text: "ComfyUI core v0.2.0 released",
- url: "https://slack.com/message/core-123",
- },
- });
+ mockCollection.findOne.mockImplementationOnce(() =>
+ Promise.resolve({
+ version: "v0.2.0",
+ slackMessage: {
+ text: "ComfyUI core v0.2.0 released",
+ url: "https://slack.com/message/core-123",
+ },
+ }),
+ );
// Second call - save with message including core version
mockCollection.findOneAndUpdate.mockResolvedValueOnce({
@@ -489,8 +489,7 @@ describe("GithubDesktopReleaseNotificationTask", () => {
};
mockGh.repos = {
- listReleases: jest
- .fn()
+ listReleases: mock()
.mockResolvedValueOnce({ data: [mockComfyUIRelease] })
.mockResolvedValueOnce({ data: [mockDesktopRelease] }),
} as any;
@@ -535,9 +534,11 @@ describe("GithubDesktopReleaseNotificationTask", () => {
};
mockGh.repos = {
- listReleases: jest.fn().mockResolvedValue({
- data: [oldRelease],
- }),
+ listReleases: mock(() =>
+ Promise.resolve({
+ data: [oldRelease],
+ }),
+ ),
} as any;
mockCollection.findOneAndUpdate.mockResolvedValue({
@@ -561,6 +562,12 @@ describe("GithubDesktopReleaseNotificationTask", () => {
describe("Database Index", () => {
it("should create unique index on url field", async () => {
+ mockGh.repos = {
+ listReleases: mock(() => Promise.resolve({ data: [] })),
+ } as any;
+
+ await runGithubDesktopReleaseNotificationTask();
+
expect(mockCollection.createIndex).toHaveBeenCalledWith({ url: 1 }, { unique: true });
});
});
diff --git a/app/tasks/gh-frontend-release-notification/index.spec.ts b/app/tasks/gh-frontend-release-notification/index.spec.ts
index 03dfc50d..e208edcf 100644
--- a/app/tasks/gh-frontend-release-notification/index.spec.ts
+++ b/app/tasks/gh-frontend-release-notification/index.spec.ts
@@ -1,40 +1,49 @@
-import { db } from "@/src/db";
-import { gh } from "@/lib/github";
import { parseGithubRepoUrl } from "@/src/parseOwnerRepo";
-import { getSlackChannel } from "@/lib/slack/channels";
-import { afterEach, beforeEach, describe, expect, it, jest } from "@jest/globals";
-import runGithubFrontendReleaseNotificationTask from "./index";
-
-jest.mock("@/src/gh");
-jest.mock("@/src/slack/channels");
-jest.mock("../gh-desktop-release-notification/upsertSlackMessage");
-
-const mockGh = gh as jest.Mocked;
-const mockGetSlackChannel = getSlackChannel as jest.MockedFunction;
-const { upsertSlackMessage } = jest.requireMock(
- "../gh-desktop-release-notification/upsertSlackMessage",
-);
+import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from "bun:test";
+
+// Mock db module BEFORE importing anything that uses it
+const mockDb = {
+ collection: mock(() => ({
+ findOne: mock(),
+ findOneAndUpdate: mock(),
+ createIndex: mock(),
+ })),
+ close: mock(),
+};
+mock.module("@/src/db", () => ({ db: mockDb }));
+mock.module("@/src/gh", () => ({ gh: {} }));
+mock.module("@/src/slack/channels", () => ({ getSlackChannel: mock() }));
+mock.module("../gh-desktop-release-notification/upsertSlackMessage", () => ({ upsertSlackMessage: mock() }));
+
+// Now we can import the modules that depend on mocked modules
+const { db } = await import("@/src/db");
+const { gh } = await import("@/src/gh");
+const { getSlackChannel } = await import("@/src/slack/channels");
+const { upsertSlackMessage } = await import("../gh-desktop-release-notification/upsertSlackMessage");
+const { default: runGithubFrontendReleaseNotificationTask } = await import("./index");
+
+const mockGh = gh as any;
+const mockGetSlackChannel = getSlackChannel as any;
+const mockUpsertSlackMessage = upsertSlackMessage as any;
describe("GithubFrontendReleaseNotificationTask", () => {
let collection: any;
beforeEach(async () => {
- jest.clearAllMocks();
-
collection = {
- findOne: jest.fn(),
- findOneAndUpdate: jest.fn(),
- createIndex: jest.fn(),
+ findOne: mock(),
+ findOneAndUpdate: mock(),
+ createIndex: mock(),
};
- jest.spyOn(db, "collection").mockReturnValue(collection);
+ spyOn(db, "collection").mockReturnValue(collection);
mockGetSlackChannel.mockResolvedValue({
id: "test-channel-id",
name: "frontend",
} as any);
- upsertSlackMessage.mockResolvedValue({
+ mockUpsertSlackMessage.mockResolvedValue({
text: "mocked message",
channel: "test-channel-id",
url: "https://slack.com/message/123",
@@ -42,7 +51,7 @@ describe("GithubFrontendReleaseNotificationTask", () => {
});
afterEach(async () => {
- jest.restoreAllMocks();
+ // Cleanup
});
describe("parseGithubRepoUrl", () => {
@@ -68,9 +77,11 @@ describe("GithubFrontendReleaseNotificationTask", () => {
};
mockGh.repos = {
- listReleases: jest.fn().mockResolvedValue({
- data: [mockRelease],
- }),
+ listReleases: mock(() =>
+ Promise.resolve({
+ data: [mockRelease],
+ }),
+ ),
} as any;
// First call - no existing message
@@ -128,9 +139,11 @@ describe("GithubFrontendReleaseNotificationTask", () => {
};
mockGh.repos = {
- listReleases: jest.fn().mockResolvedValue({
- data: [mockRelease],
- }),
+ listReleases: mock(() =>
+ Promise.resolve({
+ data: [mockRelease],
+ }),
+ ),
} as any;
// Return task with existing message text matching new message
@@ -167,9 +180,11 @@ describe("GithubFrontendReleaseNotificationTask", () => {
};
mockGh.repos = {
- listReleases: jest.fn().mockResolvedValue({
- data: [mockPrerelease],
- }),
+ listReleases: mock(() =>
+ Promise.resolve({
+ data: [mockPrerelease],
+ }),
+ ),
} as any;
// First call - save initial data
@@ -221,9 +236,11 @@ describe("GithubFrontendReleaseNotificationTask", () => {
};
mockGh.repos = {
- listReleases: jest.fn().mockResolvedValue({
- data: [mockDraft],
- }),
+ listReleases: mock(() =>
+ Promise.resolve({
+ data: [mockDraft],
+ }),
+ ),
} as any;
collection.findOneAndUpdate.mockResolvedValue({
@@ -263,9 +280,11 @@ describe("GithubFrontendReleaseNotificationTask", () => {
};
mockGh.repos = {
- listReleases: jest.fn().mockResolvedValue({
- data: [oldRelease],
- }),
+ listReleases: mock(() =>
+ Promise.resolve({
+ data: [oldRelease],
+ }),
+ ),
} as any;
collection.findOneAndUpdate.mockResolvedValue({
@@ -297,9 +316,11 @@ describe("GithubFrontendReleaseNotificationTask", () => {
};
mockGh.repos = {
- listReleases: jest.fn().mockResolvedValue({
- data: [mockRelease],
- }),
+ listReleases: mock(() =>
+ Promise.resolve({
+ data: [mockRelease],
+ }),
+ ),
} as any;
// Return task with old message text
@@ -356,9 +377,11 @@ describe("GithubFrontendReleaseNotificationTask", () => {
};
mockGh.repos = {
- listReleases: jest.fn().mockResolvedValue({
- data: [mockRelease],
- }),
+ listReleases: mock(() =>
+ Promise.resolve({
+ data: [mockRelease],
+ }),
+ ),
} as any;
// First call - save initial data
diff --git a/app/tasks/gh-issue-transfer-comfyui-to-frontend/index.spec.ts b/app/tasks/gh-issue-transfer-comfyui-to-frontend/index.spec.ts
index 320fb073..4f1e1cda 100644
--- a/app/tasks/gh-issue-transfer-comfyui-to-frontend/index.spec.ts
+++ b/app/tasks/gh-issue-transfer-comfyui-to-frontend/index.spec.ts
@@ -104,22 +104,9 @@ describe("GithubFrontendIssueTransferTask", () => {
}
return HttpResponse.json([]);
}),
- // Mock fetching comments
+ // Mock listing comments (for comment pagination)
http.get("https://api.github.com/repos/comfyanonymous/ComfyUI/issues/123/comments", () => {
- return HttpResponse.json([
- {
- id: 1,
- body: "First comment",
- user: { login: "test-user", id: 1 },
- created_at: "2025-01-11T10:00:00Z",
- },
- {
- id: 2,
- body: "Second comment",
- user: { login: "test-user-2", id: 2 },
- created_at: "2025-01-12T10:00:00Z",
- },
- ]);
+ return HttpResponse.json([]);
}),
// Mock creating issue in target repo
http.post(
@@ -134,22 +121,22 @@ describe("GithubFrontendIssueTransferTask", () => {
},
),
// Mock creating comment on source issue
- http.post(
- "https://api.github.com/repos/comfyanonymous/ComfyUI/issues/123/comments",
- async ({ request }) => {
- createdComment = await request.json();
- return HttpResponse.json({
- id: 999,
- body: createdComment.body,
- user: { login: "test-user", id: 1 },
- html_url: "https://github.com/comfyanonymous/ComfyUI/issues/123#issuecomment-999",
- created_at: new Date().toISOString(),
- });
- },
- ),
- // Mock closing the issue
+ http.post("https://api.github.com/repos/comfyanonymous/ComfyUI/issues/123/comments", async ({ request }) => {
+ createdComment = await request.json();
+ return HttpResponse.json({
+ id: 999,
+ body: createdComment.body,
+ user: { login: "test-user", id: 1 },
+ html_url: "https://github.com/comfyanonymous/ComfyUI/issues/123#issuecomment-999",
+ created_at: new Date().toISOString(),
+ });
+ }),
+ // Mock closing the source issue
http.patch("https://api.github.com/repos/comfyanonymous/ComfyUI/issues/123", () => {
- return HttpResponse.json({});
+ return HttpResponse.json({
+ ...sourceIssue,
+ state: "closed",
+ });
}),
);
@@ -162,7 +149,7 @@ describe("GithubFrontendIssueTransferTask", () => {
expect(createdIssue.body).toContain(
"*This issue is transferred from: https://github.com/comfyanonymous/ComfyUI/issues/123*",
);
- expect(createdIssue.labels).toEqual(["bug"]);
+ expect(createdIssue.labels).toEqual(["bug"]); // "frontend" label is filtered out
expect(createdIssue.assignees).toEqual(["testuser"]);
// Verify comment was posted
@@ -280,6 +267,7 @@ describe("GithubFrontendIssueTransferTask", () => {
http.get("https://api.github.com/repos/comfyanonymous/ComfyUI/issues", () => {
return HttpResponse.json([sourceIssue]);
}),
+ // Mock listing comments
http.get("https://api.github.com/repos/comfyanonymous/ComfyUI/issues/555/comments", () => {
return HttpResponse.json([]);
}),
@@ -321,6 +309,7 @@ describe("GithubFrontendIssueTransferTask", () => {
http.get("https://api.github.com/repos/comfyanonymous/ComfyUI/issues", () => {
return HttpResponse.json([sourceIssue]);
}),
+ // Mock listing comments
http.get("https://api.github.com/repos/comfyanonymous/ComfyUI/issues/666/comments", () => {
return HttpResponse.json([]);
}),
@@ -333,6 +322,7 @@ describe("GithubFrontendIssueTransferTask", () => {
http.post("https://api.github.com/repos/comfyanonymous/ComfyUI/issues/666/comments", () => {
return HttpResponse.json({ message: "Comment Error" }, { status: 403 });
}),
+ // Note: No need to mock closing since comment fails and close is in the same try block
);
await runGithubFrontendIssueTransferTask();
@@ -390,36 +380,29 @@ describe("GithubFrontendIssueTransferTask", () => {
}
return HttpResponse.json([]);
}),
- http.get(
- "https://api.github.com/repos/comfyanonymous/ComfyUI/issues/:issue_number/comments",
- () => {
- return HttpResponse.json([]);
- },
- ),
- http.post(
- "https://api.github.com/repos/Comfy-Org/ComfyUI_frontend/issues",
- async ({ request }) => {
- const body: any = await request.json();
- issuesCreated++;
- const issueNumber = parseInt(body.title.split(" ")[1]);
- return HttpResponse.json({
- number: issueNumber + 10000,
- html_url: `https://github.com/Comfy-Org/ComfyUI_frontend/issues/${issueNumber + 10000}`,
- });
- },
- ),
- http.post(
- "https://api.github.com/repos/comfyanonymous/ComfyUI/issues/:issue_number/comments",
- () => {
- commentsCreated++;
- return HttpResponse.json({
- id: commentsCreated,
- html_url: "https://github.com/comfyanonymous/ComfyUI/issues/comment",
- });
- },
- ),
+ // Mock listing comments for all issues
+ http.get("https://api.github.com/repos/comfyanonymous/ComfyUI/issues/:issue_number/comments", () => {
+ return HttpResponse.json([]);
+ }),
+ http.post("https://api.github.com/repos/Comfy-Org/ComfyUI_frontend/issues", async ({ request }) => {
+ const body: any = await request.json();
+ issuesCreated++;
+ const issueNumber = parseInt(body.title.split(" ")[1]);
+ return HttpResponse.json({
+ number: issueNumber + 10000,
+ html_url: `https://github.com/Comfy-Org/ComfyUI_frontend/issues/${issueNumber + 10000}`,
+ });
+ }),
+ http.post("https://api.github.com/repos/comfyanonymous/ComfyUI/issues/:issue_number/comments", () => {
+ commentsCreated++;
+ return HttpResponse.json({
+ id: commentsCreated,
+ html_url: "https://github.com/comfyanonymous/ComfyUI/issues/comment",
+ });
+ }),
+ // Mock closing all issues
http.patch("https://api.github.com/repos/comfyanonymous/ComfyUI/issues/:issue_number", () => {
- return HttpResponse.json({});
+ return HttpResponse.json({ state: "closed" });
}),
);
diff --git a/app/tasks/page.tsx b/app/tasks/page.tsx
index ac8b42f0..f929b1fd 100644
--- a/app/tasks/page.tsx
+++ b/app/tasks/page.tsx
@@ -1,7 +1,14 @@
import Link from "next/link";
import { Suspense } from "react";
+import { GithubBugcopTask } from "../../bot/gh-bugcop/gh-bugcop";
+import { GithubBountyTask } from "./gh-bounty/gh-bounty";
+import { GithubDesignTask } from "./gh-design/gh-design";
+import {
+ GithubContributorAnalyzeTask,
+ GithubContributorAnalyzeTaskFilter,
+} from "./github-contributor-analyze/GithubContributorAnalyzeTask";
-// Force dynamic rendering to avoid build-time database access
+// Prevent static generation since this page requires database access
export const dynamic = "force-dynamic";
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -14,11 +21,6 @@ export default async function TasksIndexPage() {
// Dynamic imports to avoid build-time execution
const { GithubActionUpdateTask } =
await import("@/src/GithubActionUpdateTask/GithubActionUpdateTask");
- const { GithubBugcopTask } = await import("../../run/gh-bugcop/gh-bugcop");
- const { GithubBountyTask } = await import("./gh-bounty/gh-bounty");
- const { GithubDesignTask } = await import("./gh-design/gh-design");
- const { GithubContributorAnalyzeTask, GithubContributorAnalyzeTaskFilter } =
- await import("./github-contributor-analyze/GithubContributorAnalyzeTask");
const Counts = {
GithubActionUpdateTask: () => (
diff --git a/app/tasks/run-gh-tasks.ts b/app/tasks/run-gh-tasks.ts
index dac80255..eeb91c8a 100644
--- a/app/tasks/run-gh-tasks.ts
+++ b/app/tasks/run-gh-tasks.ts
@@ -3,7 +3,7 @@ import { db } from "@/src/db";
import isCI from "is-ci";
// Import all the 5-minute tasks
-import runGithubBugcopTask from "../../run/gh-bugcop/gh-bugcop";
+import runGithubBugcopTask from "../../bot/gh-bugcop/gh-bugcop";
import runGithubBountyTask from "./gh-bounty/gh-bounty";
import runGithubCoreTagNotificationTask from "./gh-core-tag-notification/index";
import { runGithubDesignTask } from "./gh-design/gh-design";
diff --git a/run/.dockerignore b/bot/.dockerignore
similarity index 100%
rename from run/.dockerignore
rename to bot/.dockerignore
diff --git a/bot/Dockerfile b/bot/Dockerfile
index 81c69076..ed988fe3 100644
--- a/bot/Dockerfile
+++ b/bot/Dockerfile
@@ -1,20 +1,28 @@
-FROM snomiao/agent-yes
+# Use official Bun image
+FROM oven/bun:1.1.21-slim
# Set working directory
WORKDIR /app
-# Copy package files and patches (build context is project root)
-COPY package.json bun.lock* ./
-COPY patches ./patches
+# Copy package files
+COPY package.json bun.lock ./
# Install dependencies
-RUN bun install --frozen-lockfile
+RUN bun install --production --ignore-scripts
-# Copy entire project
-COPY . ./
+# Copy source code
+COPY ../ .
-# Set entrypoint to load nvm
-ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
+# Expose port
+EXPOSE 8080
-# Run the bot service
-CMD ["bun", "bot/index.ts"]
+# Set environment variables
+ENV NODE_ENV=production
+ENV PORT=8080
+
+# Health check
+HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
+ CMD curl -f http://localhost:8080/health || exit 1
+
+# Run the webhook service
+CMD ["bun", "bot/index.tsx"]
\ No newline at end of file
diff --git a/bot/README.md b/bot/README.md
index d3f99c52..89313a96 100644
--- a/bot/README.md
+++ b/bot/README.md
@@ -1,166 +1,285 @@
-# Bot Skills
+# GitHub Webhook Service Deployment
-This directory contains the skills/utilities used by the ComfyPR Bot to interact with Slack, Notion, and other services.
+This directory contains the GitHub webhook service and deployment configuration for Google Cloud Run.
-## Unified CLI
+## Overview
-A consolidated CLI is available at `bot/cli.ts` using yargs. It exposes GitHub PR-bot actions and Slack/Notion utilities via a single entry point.
+The webhook service (`index.tsx`) monitors GitHub repositories for events like issues, pull requests, and comments. It can operate in two modes:
-Run with Bun:
+- **Webhook mode**: Real-time event handling via GitHub webhooks
+- **Polling mode**: Fallback polling when webhooks can't be configured
+
+## Prerequisites
+
+1. **Google Cloud CLI**: Install and authenticate with `gcloud auth login`
+2. **Docker**: Required for building container images
+3. **Project Setup**: Enable billing on your Google Cloud project
+4. **GitHub Token**: Personal Access Token with repo permissions
+5. **Webhook Secret**: Secret for securing webhook payloads
+
+## Environment Variables
+
+The service requires these environment variables:
+
+### Required
+
+- `GITHUB_TOKEN`: GitHub Personal Access Token
+- `GITHUB_WEBHOOK_SECRET`: Secret for webhook signature validation
+- `PORT`: Server port (defaults to 8080)
+
+### Optional
+
+- `GITHUB_WEBHOOK_BASEURL`: Base URL for webhook endpoints
+- `GITHUB_WEBHOOK_PORT`: Alternative port setting
+
+## Deployment
+
+### Quick Deploy
```bash
-bun bot/cli.ts --help
+# Set environment variables
+export GOOGLE_CLOUD_PROJECT="your-project-id"
+export GITHUB_TOKEN="ghp_your_token_here"
+export GITHUB_WEBHOOK_SECRET="your_webhook_secret"
+
+# Deploy
+cd run
+./deploy.sh
```
-Common commands:
+### Manual Deploy Steps
-```bash
-# Create a coding sub-agent and open a PR
-bun bot/cli.ts github pr -r Comfy-Org/ComfyUI -b main -p "Fix auth bug"
+1. **Enable APIs**:
-# Alias for the above
-bun bot/cli.ts pr -r Comfy-Org/desktop -p "Add spellcheck to editor"
+ ```bash
+ gcloud services enable cloudbuild.googleapis.com run.googleapis.com artifactregistry.googleapis.com secretmanager.googleapis.com
+ ```
-# Slack utilities
-bun bot/cli.ts slack update -c C123 -t 1234567890.123456 -m "Working on it"
-bun bot/cli.ts slack read-thread -c C123 -t 1234567890.123456 -l 50
+2. **Create Artifact Registry**:
-# Notion search
-bun bot/cli.ts notion search -q "ComfyUI setup" -l 5
-```
+ ```bash
+ gcloud artifacts repositories create github-webhook-service \
+ --repository-format=docker \
+ --location=us-central1
+ ```
+
+3. **Create Secrets**:
+
+ ```bash
+ echo "$GITHUB_TOKEN" | gcloud secrets create github-token --data-file=-
+ echo "$GITHUB_WEBHOOK_SECRET" | gcloud secrets create github-webhook-secret --data-file=-
+ ```
+
+4. **Build and Deploy**:
+ ```bash
+ gcloud builds submit . --config=cloudbuild.yaml
+ ```
-Environment requirements:
+## Files
-- GitHub PR agent: token/config as required by existing `bot/github` tools
-- Slack: `SLACK_BOT_TOKEN`, `SLACK_SOCKET_TOKEN` (for socket mode)
-- Notion: `NOTION_TOKEN`
+- `index.tsx` - Main webhook service application
+- `Dockerfile` - Container build configuration
+- `cloudbuild.yaml` - Cloud Build deployment configuration
+- `deploy.sh` - Automated deployment script
+- `README.md` - This documentation
-## Slack Skills
+## Service Endpoints
-### msg-update.ts
+- `/` - Root endpoint with basic info
+- `/api/github/webhook` - GitHub webhook endpoint
+- `/health` - Health check endpoint
-Update an existing Slack message.
+## GitHub Webhook Configuration
-**Usage:**
+After deployment, configure your GitHub repositories:
+
+1. Go to Repository Settings â Webhooks
+2. Add webhook with URL: `https://your-service-url/api/github/webhook`
+3. Set content type to `application/json`
+4. Enter your webhook secret
+5. Select events: Issues, Pull requests, Issue comments, Pull request reviews, Labels
+
+## Local Development
```bash
-bun bot/slack/msg-update.ts --channel --ts --text ""
+# Install dependencies
+bun install
+
+# Set environment variables
+export GITHUB_TOKEN="your_token"
+export GITHUB_WEBHOOK_SECRET="your_secret"
+export PORT=3000
+
+# Run locally
+bun run index.tsx
```
-**Example:**
+## Getting Your Webhook Secret
+
+### Option 1: Generate a Secure Random Secret (Recommended)
```bash
-bun bot/slack/msg-update.ts --channel C123ABC --ts 1234567890.123456 --text "Updated message content"
+# Generate a 32-byte random hex string
+node -e "console.log('GITHUB_WEBHOOK_SECRET='+require('crypto').randomBytes(32).toString('hex'))" >> .env.local
+
+# Or use openssl
+openssl rand -hex 32
+
+# Or use Bun
+bun -e "console.log(crypto.randomBytes(32).toString('hex'))"
```
-**Environment Variables:**
+Example output: `a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456`
-- `SLACK_BOT_TOKEN`: Your Slack bot token
+### Option 2: Use a Password Generator
-### msg-read-thread.ts
+Visit [1Password Generator](https://1password.com/password-generator/) or similar:
-Read all messages from a Slack thread.
+- Length: 64 characters
+- Include: Letters, Numbers, Symbols
+- Copy the generated password
-**Usage:**
+### Option 3: Custom Secret (Less Secure)
```bash
-bun bot/slack/msg-read-thread.ts --channel --ts [--limit ]
+export GITHUB_WEBHOOK_SECRET="my-super-secret-webhook-password-2024"
```
-**Example:**
+â ī¸ **Security Note**: Use a long, random, unique secret that you don't use anywhere else.
+
+## Environment Variables
+
+| Variable | Required | Description | Example |
+| ----------------------- | ------------ | ------------------------------- | ------------------------ |
+| `USE_WEBHOOKS` | No | Enable webhook mode | `true` |
+| `GITHUB_WEBHOOK_SECRET` | Webhook mode | Secret for webhook verification | `abc123...` |
+| `WEBHOOK_BASE_URL` | Webhook mode | Your public URL | `https://myapp.ngrok.io` |
+| `GH_TOKEN` | Yes | GitHub token with repo access | `ghp_xxx...` |
+| `PORT` | No | Server port | `3000` |
+
+## GitHub Token Setup
+
+1. Go to [GitHub Personal Access Tokens](https://github.com/settings/tokens)
+2. Click "Generate new token" â "Generate new token (classic)"
+3. Select scopes:
+ - â
`repo` (Full control of private repositories)
+ - â
`admin:repo_hook` (Read and write repository hooks)
+4. Copy the token and set it as `GH_TOKEN`
```bash
-bun bot/slack/msg-read-thread.ts --channel C123ABC --ts 1234567890.123456 --limit 50
+export GH_TOKEN=ghp_your_github_token_here
```
-**Environment Variables:**
+## Making Your Server Public (For Webhooks)
-- `SLACK_BOT_TOKEN`: Your Slack bot token
+### Development - Using ngrok
-### parseSlackMessageToMarkdown.ts
+1. Install ngrok: https://ngrok.com/download
+2. Start your server: `bun bot/gh-service.tsx`
+3. In another terminal: `ngrok http 3000`
+4. Copy the HTTPS URL: `https://abc123.ngrok.io`
+5. Set environment variable: `export WEBHOOK_BASE_URL=https://abc123.ngrok.io`
-Utility function to convert Slack message formatting to Markdown.
+### Development - Using Cloudflare Tunnel
-**Features:**
+```bash
+# Install cloudflared
+npm install -g @cloudflare/next-on-pages
-- User mentions: `<@U123>` â `@U123`
-- Channel mentions: `<#C456|general>` â `#general`
-- Links: `` â `[text](https://example.com)`
-- Bold: `*text*` â `**text**`
-- Italic: `_text_` â `*text*`
-- Preserves code blocks and inline code
+# Start tunnel
+cloudflared tunnel --url http://localhost:3000
-**Usage:**
+# Copy the HTTPS URL and set WEBHOOK_BASE_URL
+```
-```typescript
-import { parseSlackMessageToMarkdown } from "./bot/slack/parseSlackMessageToMarkdown";
+### Production Deployment
-const markdown = await parseSlackMessageToMarkdown("Hello <@U123> with *bold* text");
-```
+Deploy to any cloud provider:
-### slackTsToISO.ts
+- **Vercel**: `vercel --prod`
+- **Railway**: `railway deploy`
+- **Heroku**: `git push heroku main`
+- **DigitalOcean**: App Platform
+- **AWS**: Lambda/EC2
+- **Google Cloud**: Cloud Run
-Convert Slack timestamp to ISO 8601 format.
+## Complete Setup Example
-**Usage:**
+```bash
+# 1. Generate webhook secret
+export GITHUB_WEBHOOK_SECRET=$(node -e "console.log(require('crypto').randomBytes(32).toString('hex'))")
+
+# 2. Set your GitHub token
+export GH_TOKEN=ghp_your_github_personal_access_token
+
+# 3. Make server public (development)
+ngrok http 3000 & # Run in background
+export WEBHOOK_BASE_URL=https://your-ngrok-url.ngrok.io
-```typescript
-import { slackTsToISO } from "./bot/slack/slackTsToISO";
+# 4. Enable webhook mode
+export USE_WEBHOOKS=true
-const iso = slackTsToISO("1703347200.123456");
-// Returns: "2023-12-23T16:00:00.123Z"
+# 5. Start the monitor
+bun bot/gh-service.tsx
```
-## Notion Skills
+## Event Types Monitored
-### notion/search.ts
+- đ **New Issues** - When someone opens an issue
+- đ **New Pull Requests** - When someone creates a PR
+- đŦ **Comments** - Issue comments, PR comments, review comments
+- đˇī¸ **Labels** - When labels are added or removed
+- â
**Status Changes** - Issue/PR closed, reopened, merged
-Search Notion pages in the Comfy-Org workspace.
+## Webhook vs Polling Comparison
-**Usage:**
+| Feature | Polling (30s) | Webhooks |
+| ---------------------- | ----------------- | -------------------- |
+| **Latency** | ~30 seconds | ~1 second |
+| **API Rate Limit** | High usage | Minimal usage |
+| **Setup Complexity** | Simple | Moderate |
+| **Reliability** | Always works | Network dependent |
+| **Resource Usage** | Higher CPU/Memory | Lower |
+| **GitHub Permissions** | Read-only | Admin/Write required |
-```bash
-bun bot/notion/search.ts --query "" [--limit ]
-```
+## Troubleshooting
-**Example:**
+### "Error creating webhook: 403 Forbidden"
-```bash
-bun bot/notion/search.ts --query "ComfyUI setup" --limit 5
-```
+- Your GitHub token needs `admin:repo_hook` permissions
+- You need admin or write access to the repositories
-**Environment Variables:**
+### "Webhook endpoint not reachable"
-- `NOTION_TOKEN`: Your Notion integration token
+- Ensure your `WEBHOOK_BASE_URL` is publicly accessible
+- Test: `curl -X POST https://your-url.com/webhook`
-**Output:**
-Returns a JSON array of matching pages with title, URL, and timestamps.
+### "Webhook signature verification failed"
-## Testing
+- Check that `GITHUB_WEBHOOK_SECRET` matches what you configured
+- Ensure the secret is the same when creating webhooks
-Test individual utilities:
+### No webhook events received
-```bash
-# Test Slack timestamp conversion
-bun bot/slack/slackTsToISO.ts
+- Check GitHub webhook delivery logs in repo settings â Webhooks
+- Verify the webhook URL is correct: `https://your-domain.com/webhook`
+- Ensure your server is running and accessible
-# Test Slack to Markdown parsing
-bun bot/slack/parseSlackMessageToMarkdown.ts
-```
+## Security Best Practices
-## Development
+1. **Use strong webhook secrets** - Generate random 64+ character strings
+2. **Use HTTPS** - Never use HTTP for webhook endpoints in production
+3. **Verify signatures** - The system automatically verifies webhook authenticity
+4. **Rotate secrets periodically** - Update webhook secrets every 90 days
+5. **Limit token permissions** - Only grant necessary GitHub token scopes
-All scripts follow the standard development pattern outlined in CLAUDE.md:
+## Monitored Repositories
-1. TypeScript with full type safety
-2. Executable with `bun ` when `import.meta.main` is true
-3. Exportable functions for use as libraries
-4. Command-line argument parsing with `yargs` in `bot/cli.ts` and `parseArgs` in leaf tools
-5. Proper error handling and validation
-6. Cached API clients from `@/lib`
+The system monitors these repositories by default:
-## Notes
+- `Comfy-Org/Comfy-PR`
+- `comfyanonymous/ComfyUI`
+- `Comfy-Org/ComfyUI_frontend`
+- `Comfy-Org/desktop`
-- All Slack and Notion API calls are automatically cached using the cached clients from `@/lib`
-- Cache is stored in `node_modules/.cache/` directory
-- Scripts can be used both as standalone CLI tools and as importable modules
+To modify the list, edit the `REPOLIST` array in `gh-service.tsx`.
diff --git a/run/cloudbuild.yaml b/bot/cloudbuild.yaml
similarity index 98%
rename from run/cloudbuild.yaml
rename to bot/cloudbuild.yaml
index 68640ea2..a2907de3 100644
--- a/run/cloudbuild.yaml
+++ b/bot/cloudbuild.yaml
@@ -6,7 +6,7 @@ steps:
- "-t"
- "${_LOCATION}-docker.pkg.dev/${PROJECT_ID}/${_REPOSITORY}/github-webhook-service:latest"
- "-f"
- - "run/Dockerfile"
+ - "bot/Dockerfile"
- "."
id: "build-image"
diff --git a/run/deploy.sh b/bot/deploy.sh
similarity index 99%
rename from run/deploy.sh
rename to bot/deploy.sh
index 27738502..b7e0098f 100755
--- a/run/deploy.sh
+++ b/bot/deploy.sh
@@ -111,7 +111,7 @@ deploy_service() {
WEBHOOK_BASE_URL="https://${SERVICE_NAME}-REPLACE_WITH_HASH-${LOCATION:0:2}.a.run.app"
gcloud builds submit . \
- --config=run/cloudbuild.yaml \
+ --config=bot/cloudbuild.yaml \
--project="$PROJECT_ID" \
--substitutions="_LOCATION=$LOCATION,_REPOSITORY=$REPOSITORY,_SERVICE_NAME=$SERVICE_NAME,_WEBHOOK_BASE_URL=$WEBHOOK_BASE_URL"
diff --git a/run/easylabel.tsx b/bot/easylabel.tsx
similarity index 100%
rename from run/easylabel.tsx
rename to bot/easylabel.tsx
diff --git a/run/gh-bugcop/GithubBugcopTaskStatus.tsx b/bot/gh-bugcop/GithubBugcopTaskStatus.tsx
similarity index 100%
rename from run/gh-bugcop/GithubBugcopTaskStatus.tsx
rename to bot/gh-bugcop/GithubBugcopTaskStatus.tsx
diff --git a/run/gh-bugcop/gh-bugcop.tsx b/bot/gh-bugcop/gh-bugcop.tsx
similarity index 100%
rename from run/gh-bugcop/gh-bugcop.tsx
rename to bot/gh-bugcop/gh-bugcop.tsx
diff --git a/run/gh-bugcop/page.tsx b/bot/gh-bugcop/page.tsx
similarity index 100%
rename from run/gh-bugcop/page.tsx
rename to bot/gh-bugcop/page.tsx
diff --git a/run/ghUrl.tsx b/bot/ghUrl.tsx
similarity index 100%
rename from run/ghUrl.tsx
rename to bot/ghUrl.tsx
diff --git a/bot/github-webhook-event-type/index.ts b/bot/github-webhook-event-type/index.ts
new file mode 100644
index 00000000..629a7caa
--- /dev/null
+++ b/bot/github-webhook-event-type/index.ts
@@ -0,0 +1,159 @@
+import type { components as GithubApiComponents } from "@octokit/openapi-types";
+type S = GithubApiComponents["schemas"];
+export type WEBHOOK_EVENTS = {
+ branch_protection_configuration: S[`webhook-branch-protection-configuration${string}` & keyof S];
+ branch_protection_rule: S[`webhook-branch-protection-rule${string}` & keyof S];
+ check_run: S[`webhook-check-run${string}` & keyof S];
+ check_suite: S[`webhook-check-suite${string}` & keyof S];
+ code_scanning_alert: S[`webhook-code-scanning-alert${string}` & keyof S];
+ commit_comment: S[`webhook-commit-comment${string}` & keyof S];
+ create: S[`webhook-create${string}` & keyof S];
+ custom_property: S[`webhook-custom-property${string}` & keyof S];
+ custom_property_values: S[`webhook-custom-property-values${string}` & keyof S];
+ delete: S[`webhook-delete${string}` & keyof S];
+ dependabot_alert: S[`webhook-dependabot-alert${string}` & keyof S];
+ deploy_key: S[`webhook-deploy-key${string}` & keyof S];
+ deployment: S[`webhook-deployment${string}` & keyof S];
+ deployment_protection_rule: S[`webhook-deployment-protection-rule${string}` & keyof S];
+ deployment_review: S[`webhook-deployment-review${string}` & keyof S];
+ deployment_status: S[`webhook-deployment-status${string}` & keyof S];
+ discussion: S[`webhook-discussion${string}` & keyof S];
+ discussion_comment: S[`webhook-discussion-comment${string}` & keyof S];
+ fork: S[`webhook-fork${string}` & keyof S];
+ github_app_authorization: S[`webhook-github-app-authorization${string}` & keyof S];
+ gollum: S[`webhook-gollum${string}` & keyof S];
+ installation: S[`webhook-installation${string}` & keyof S];
+ installation_repositories: S[`webhook-installation-repositories${string}` & keyof S];
+ installation_target: S[`webhook-installation-target${string}` & keyof S];
+ issue_comment: S[`webhook-issue-comment${string}` & keyof S];
+ issue_dependencies: S[`webhook-issue-dependencies${string}` & keyof S];
+ issues: S[`webhook-issues${string}` & keyof S];
+ label: S[`webhook-label${string}` & keyof S];
+ marketplace_purchase: S[`webhook-marketplace-purchase${string}` & keyof S];
+ member: S[`webhook-member${string}` & keyof S];
+ membership: S[`webhook-membership${string}` & keyof S];
+ merge_group: S[`webhook-merge-group${string}` & keyof S];
+ meta: S[`webhook-meta${string}` & keyof S];
+ milestone: S[`webhook-milestone${string}` & keyof S];
+ org_block: S[`webhook-org-block${string}` & keyof S];
+ organization: S[`webhook-organization${string}` & keyof S];
+ package: S[`webhook-package${string}` & keyof S];
+ page_build: S[`webhook-page-build${string}` & keyof S];
+ personal_access_token_request: S[`webhook-personal-access-token-request${string}` & keyof S];
+ ping: S[`webhook-ping${string}` & keyof S];
+ project: S[`webhook-project${string}` & keyof S];
+ project_card: S[`webhook-project-card${string}` & keyof S];
+ project_column: S[`webhook-project-column${string}` & keyof S];
+ projects_v2: S[`webhook-projects-v2${string}` & keyof S];
+ projects_v2_item: S[`webhook-projects-v2-item${string}` & keyof S];
+ projects_v2_status_update: S[`webhook-projects-v2-status-update${string}` & keyof S];
+ public: S[`webhook-public${string}` & keyof S];
+ pull_request: S[`webhook-pull-request${string}` & keyof S];
+ pull_request_review: S[`webhook-pull-request-review${string}` & keyof S];
+ pull_request_review_comment: S[`webhook-pull-request-review-comment${string}` & keyof S];
+ pull_request_review_thread: S[`webhook-pull-request-review-thread${string}` & keyof S];
+ push: S[`webhook-push${string}` & keyof S];
+ registry_package: S[`webhook-registry-package${string}` & keyof S];
+ release: S[`webhook-release${string}` & keyof S];
+ repository: S[`webhook-repository${string}` & keyof S];
+ repository_advisory: S[`webhook-repository-advisory${string}` & keyof S];
+ repository_dispatch: S[`webhook-repository-dispatch${string}` & keyof S];
+ repository_import: S[`webhook-repository-import${string}` & keyof S];
+ repository_ruleset: S[`webhook-repository-ruleset${string}` & keyof S];
+ repository_vulnerability_alert: S[`webhook-repository-vulnerability-alert${string}` & keyof S];
+ secret_scanning_alert: S[`webhook-secret-scanning-alert${string}` & keyof S];
+ secret_scanning_alert_location: S[`webhook-secret-scanning-alert-location${string}` & keyof S];
+ secret_scanning_scan: S[`webhook-secret-scanning-scan${string}` & keyof S];
+ security_advisory: S[`webhook-security-advisory${string}` & keyof S];
+ security_and_analysis: S[`webhook-security-and-analysis${string}` & keyof S];
+ sponsorship: S[`webhook-sponsorship${string}` & keyof S];
+ star: S[`webhook-star${string}` & keyof S];
+ status: S[`webhook-status${string}` & keyof S];
+ sub_issues: S[`webhook-sub-issues${string}` & keyof S];
+ team: S[`webhook-team${string}` & keyof S];
+ team_add: S[`webhook-team-add${string}` & keyof S];
+ watch: S[`webhook-watch${string}` & keyof S];
+ workflow_dispatch: S[`webhook-workflow-dispatch${string}` & keyof S];
+ workflow_job: S[`webhook-workflow-job${string}` & keyof S];
+ workflow_run: S[`webhook-workflow-run${string}` & keyof S];
+};
+// export type WEBHOOK_EVENT = {
+// type: K;
+// payload: WEBHOOK_EVENTS[K];
+// };
+export type WEBHOOK_EVENT =
+ | { type: "branch_protection_configuration"; payload: WEBHOOK_EVENTS["branch_protection_configuration"] }
+ | { type: "branch_protection_rule"; payload: WEBHOOK_EVENTS["branch_protection_rule"] }
+ | { type: "check_run"; payload: WEBHOOK_EVENTS["check_run"] }
+ | { type: "check_suite"; payload: WEBHOOK_EVENTS["check_suite"] }
+ | { type: "code_scanning_alert"; payload: WEBHOOK_EVENTS["code_scanning_alert"] }
+ | { type: "commit_comment"; payload: WEBHOOK_EVENTS["commit_comment"] }
+ | { type: "create"; payload: WEBHOOK_EVENTS["create"] }
+ | { type: "custom_property"; payload: WEBHOOK_EVENTS["custom_property"] }
+ | { type: "custom_property_values"; payload: WEBHOOK_EVENTS["custom_property_values"] }
+ | { type: "delete"; payload: WEBHOOK_EVENTS["delete"] }
+ | { type: "dependabot_alert"; payload: WEBHOOK_EVENTS["dependabot_alert"] }
+ | { type: "deploy_key"; payload: WEBHOOK_EVENTS["deploy_key"] }
+ | { type: "deployment"; payload: WEBHOOK_EVENTS["deployment"] }
+ | { type: "deployment_protection_rule"; payload: WEBHOOK_EVENTS["deployment_protection_rule"] }
+ | { type: "deployment_review"; payload: WEBHOOK_EVENTS["deployment_review"] }
+ | { type: "deployment_status"; payload: WEBHOOK_EVENTS["deployment_status"] }
+ | { type: "discussion"; payload: WEBHOOK_EVENTS["discussion"] }
+ | { type: "discussion_comment"; payload: WEBHOOK_EVENTS["discussion_comment"] }
+ | { type: "fork"; payload: WEBHOOK_EVENTS["fork"] }
+ | { type: "github_app_authorization"; payload: WEBHOOK_EVENTS["github_app_authorization"] }
+ | { type: "gollum"; payload: WEBHOOK_EVENTS["gollum"] }
+ | { type: "installation"; payload: WEBHOOK_EVENTS["installation"] }
+ | { type: "installation_repositories"; payload: WEBHOOK_EVENTS["installation_repositories"] }
+ | { type: "installation_target"; payload: WEBHOOK_EVENTS["installation_target"] }
+ | { type: "issue_comment"; payload: WEBHOOK_EVENTS["issue_comment"] }
+ | { type: "issue_dependencies"; payload: WEBHOOK_EVENTS["issue_dependencies"] }
+ | { type: "issues"; payload: WEBHOOK_EVENTS["issues"] }
+ | { type: "label"; payload: WEBHOOK_EVENTS["label"] }
+ | { type: "marketplace_purchase"; payload: WEBHOOK_EVENTS["marketplace_purchase"] }
+ | { type: "member"; payload: WEBHOOK_EVENTS["member"] }
+ | { type: "membership"; payload: WEBHOOK_EVENTS["membership"] }
+ | { type: "merge_group"; payload: WEBHOOK_EVENTS["merge_group"] }
+ | { type: "meta"; payload: WEBHOOK_EVENTS["meta"] }
+ | { type: "milestone"; payload: WEBHOOK_EVENTS["milestone"] }
+ | { type: "org_block"; payload: WEBHOOK_EVENTS["org_block"] }
+ | { type: "organization"; payload: WEBHOOK_EVENTS["organization"] }
+ | { type: "package"; payload: WEBHOOK_EVENTS["package"] }
+ | { type: "page_build"; payload: WEBHOOK_EVENTS["page_build"] }
+ | { type: "personal_access_token_request"; payload: WEBHOOK_EVENTS["personal_access_token_request"] }
+ | { type: "ping"; payload: WEBHOOK_EVENTS["ping"] }
+ | { type: "project"; payload: WEBHOOK_EVENTS["project"] }
+ | { type: "project_card"; payload: WEBHOOK_EVENTS["project_card"] }
+ | { type: "project_column"; payload: WEBHOOK_EVENTS["project_column"] }
+ | { type: "projects_v2"; payload: WEBHOOK_EVENTS["projects_v2"] }
+ | { type: "projects_v2_item"; payload: WEBHOOK_EVENTS["projects_v2_item"] }
+ | { type: "projects_v2_status_update"; payload: WEBHOOK_EVENTS["projects_v2_status_update"] }
+ | { type: "public"; payload: WEBHOOK_EVENTS["public"] }
+ | { type: "pull_request"; payload: WEBHOOK_EVENTS["pull_request"] }
+ | { type: "pull_request_review"; payload: WEBHOOK_EVENTS["pull_request_review"] }
+ | { type: "pull_request_review_comment"; payload: WEBHOOK_EVENTS["pull_request_review_comment"] }
+ | { type: "pull_request_review_thread"; payload: WEBHOOK_EVENTS["pull_request_review_thread"] }
+ | { type: "push"; payload: WEBHOOK_EVENTS["push"] }
+ | { type: "registry_package"; payload: WEBHOOK_EVENTS["registry_package"] }
+ | { type: "release"; payload: WEBHOOK_EVENTS["release"] }
+ | { type: "repository"; payload: WEBHOOK_EVENTS["repository"] }
+ | { type: "repository_advisory"; payload: WEBHOOK_EVENTS["repository_advisory"] }
+ | { type: "repository_dispatch"; payload: WEBHOOK_EVENTS["repository_dispatch"] }
+ | { type: "repository_import"; payload: WEBHOOK_EVENTS["repository_import"] }
+ | { type: "repository_ruleset"; payload: WEBHOOK_EVENTS["repository_ruleset"] }
+ | { type: "repository_vulnerability_alert"; payload: WEBHOOK_EVENTS["repository_vulnerability_alert"] }
+ | { type: "secret_scanning_alert"; payload: WEBHOOK_EVENTS["secret_scanning_alert"] }
+ | { type: "secret_scanning_alert_location"; payload: WEBHOOK_EVENTS["secret_scanning_alert_location"] }
+ | { type: "secret_scanning_scan"; payload: WEBHOOK_EVENTS["secret_scanning_scan"] }
+ | { type: "security_advisory"; payload: WEBHOOK_EVENTS["security_advisory"] }
+ | { type: "security_and_analysis"; payload: WEBHOOK_EVENTS["security_and_analysis"] }
+ | { type: "sponsorship"; payload: WEBHOOK_EVENTS["sponsorship"] }
+ | { type: "star"; payload: WEBHOOK_EVENTS["star"] }
+ | { type: "status"; payload: WEBHOOK_EVENTS["status"] }
+ | { type: "sub_issues"; payload: WEBHOOK_EVENTS["sub_issues"] }
+ | { type: "team"; payload: WEBHOOK_EVENTS["team"] }
+ | { type: "team_add"; payload: WEBHOOK_EVENTS["team_add"] }
+ | { type: "watch"; payload: WEBHOOK_EVENTS["watch"] }
+ | { type: "workflow_dispatch"; payload: WEBHOOK_EVENTS["workflow_dispatch"] }
+ | { type: "workflow_job"; payload: WEBHOOK_EVENTS["workflow_job"] }
+ | { type: "workflow_run"; payload: WEBHOOK_EVENTS["workflow_run"] };
diff --git a/bot/index.ts b/bot/index.ts
index abcb1bfb..e69de29b 100644
--- a/bot/index.ts
+++ b/bot/index.ts
@@ -1,14 +0,0 @@
-#!/usr/bin/env bun
-/**
- * ComfyPR Bot
- *
- * Slack Bot
- * @author snomiao
- */
-
-// supports only slack now
-if (import.meta.main) {
- console.log("Starting ComfyPR Slack Bot...");
- const client = await (await import("./slack-bot.ts")).startSlackBot();
- console.log("ComfyPR Slack Bot Done.");
-}
diff --git a/run/onIssueComment.tsx b/bot/onIssueComment.tsx
similarity index 100%
rename from run/onIssueComment.tsx
rename to bot/onIssueComment.tsx
diff --git a/run/onTextUpdateEvent.tsx b/bot/onTextUpdateEvent.tsx
similarity index 100%
rename from run/onTextUpdateEvent.tsx
rename to bot/onTextUpdateEvent.tsx
diff --git a/run/index.ts b/bot/v1.ts
similarity index 72%
rename from run/index.ts
rename to bot/v1.ts
index e2177676..bb0600ad 100644
--- a/run/index.ts
+++ b/bot/v1.ts
@@ -1,17 +1,15 @@
-#!/usr/bin/env bun
import KeyvSqlite from "@keyv/sqlite";
-import type { WebhookEventMap } from "@octokit/webhooks-types";
import DIE from "@snomiao/die";
-import * as crypto from "crypto";
+import crypto from "crypto";
import Keyv from "keyv";
import sflow, { pageFlow } from "sflow";
import { match, P } from "ts-pattern";
-import type { UnionToIntersection } from "type-fest";
-import { gh, type GH } from "@/lib/github";
-import { ghc } from "@/lib/github/githubCached";
-import { parseGithubRepoUrl } from "@/src/parseOwnerRepo";
-import { processIssueCommentForLableops } from "./easylabel";
-
+import { type UnionToIntersection } from "type-fest";
+import { gh, type GH } from "../src/gh/index.js";
+import { ghc } from "../src/ghc.js";
+import { parseGithubRepoUrl } from "../src/parseOwnerRepo.js";
+import { processIssueCommentForLableops } from "./easylabel.jsx";
+import type { WEBHOOK_EVENT } from "./github-webhook-event-type/index.js";
export const REPOLIST = [
"https://github.com/Comfy-Org/Comfy-PR",
"https://github.com/comfyanonymous/ComfyUI",
@@ -33,8 +31,7 @@ type WebhookIssue = GH[`webhook-issues-${string}` & keyof GH];
type WebhookIssueComment = GH[`webhook-issue-comment-${string}` & keyof GH];
type WebhookPullRequest = GH[`webhook-pull-request-${string}` & keyof GH];
type WebhookPullRequestReview = GH[`webhook-pull-request-review${string}` & keyof GH];
-type WebhookPullRequestReviewComment = GH[`webhook-pull-request-review-comment-${string}` &
- keyof GH];
+type WebhookPullRequestReviewComment = GH[`webhook-pull-request-review-comment-${string}` & keyof GH];
type HaveBody = T extends { issue: { body: string } } ? T : never;
type test = HaveBody;
type WebhookAll = GH[`webhook-${string}-${string}` & keyof GH];
@@ -49,47 +46,14 @@ type Webhook =
class RepoEventMonitor {
private monitorState = new Map();
private stateCache: Keyv;
- private commentCache: Keyv