kaneo (minimalist kanban) fork to experiment adding a tangled integration github.com/usekaneo/kaneo
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: GitHub integration (#323)

* chore(deps): bump @hookform/resolvers from 3.10.0 to 5.1.1 in /apps/web

Bumps [@hookform/resolvers](https://github.com/react-hook-form/resolvers) from 3.10.0 to 5.1.1.
- [Release notes](https://github.com/react-hook-form/resolvers/releases)
- [Commits](https://github.com/react-hook-form/resolvers/compare/v3.10.0...v5.1.1)

---
updated-dependencies:
- dependency-name: "@hookform/resolvers"
dependency-version: 5.1.1
dependency-type: direct:production
update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): update zod to version 3.25.6

* refactor: replace Error with HTTPException

* chore(ci): specify biome version 1.9.4 in CI workflow

* chore(ci): update Biome version to 1.9.7 in CI workflow

* feat: enhance project settings form with unsaved changes warning

* feat: implement GitHub integration with create, delete, and verify functionalities

* chore: update build script in package.json to output ESM format

* chore: refine build script in package.json to include additional external packages

* chore(deps): update @octokit/webhooks and octokit versions in package.json and pnpm-lock.yaml

* refactor: remove unused repository ID retrieval and update webhook URLs in documentation

* feat: add GitHub app info endpoint and integrate it into the repository browser modal

* feat: increase repository listing limit to 500 for GitHub app integration

* feat: enhance GitHub comment formatting and task description handling

* feat: add GitHub integration documentation and setup guide enhancements

* chore(deps): bump @hookform/resolvers from 3.10.0 to 5.1.1 in /apps/web (#318)

* chore(deps): bump @hookform/resolvers from 3.10.0 to 5.1.1 in /apps/web

Bumps [@hookform/resolvers](https://github.com/react-hook-form/resolvers) from 3.10.0 to 5.1.1.
- [Release notes](https://github.com/react-hook-form/resolvers/releases)
- [Commits](https://github.com/react-hook-form/resolvers/compare/v3.10.0...v5.1.1)

---
updated-dependencies:
- dependency-name: "@hookform/resolvers"
dependency-version: 5.1.1
dependency-type: direct:production
update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): update zod to version 3.25.6

* refactor: replace Error with HTTPException

* chore(ci): specify biome version 1.9.4 in CI workflow

* chore(ci): update Biome version to 1.9.7 in CI workflow

* feat: enhance project settings form with unsaved changes warning

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Andrej <aacevski@gmail.com>

* chore(release): v1.0.0

* fix: resolve merge conflicts in pnpm-lock.yaml and update @types/node dependencies

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

authored by

Andrej
dependabot[bot]
Andrej
github-actions[bot]
and committed by
GitHub
1457e37b 9a6c7e35

+3806 -11
+13
apps/api/drizzle/0001_natural_robin_chapel.sql
··· 1 + CREATE TABLE "github_integration" ( 2 + "id" text PRIMARY KEY NOT NULL, 3 + "project_id" text NOT NULL, 4 + "repository_owner" text NOT NULL, 5 + "repository_name" text NOT NULL, 6 + "installation_id" integer, 7 + "is_active" boolean DEFAULT true, 8 + "created_at" timestamp DEFAULT now() NOT NULL, 9 + "updated_at" timestamp DEFAULT now() NOT NULL, 10 + CONSTRAINT "github_integration_project_id_unique" UNIQUE("project_id") 11 + ); 12 + --> statement-breakpoint 13 + ALTER TABLE "github_integration" ADD CONSTRAINT "github_integration_project_id_project_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."project"("id") ON DELETE cascade ON UPDATE cascade;
+768
apps/api/drizzle/meta/0001_snapshot.json
··· 1 + { 2 + "id": "d516f340-217e-4840-8e91-65c778e344d1", 3 + "prevId": "1ed2e748-68b8-4d66-9d11-c06fba31a8dd", 4 + "version": "7", 5 + "dialect": "postgresql", 6 + "tables": { 7 + "public.activity": { 8 + "name": "activity", 9 + "schema": "", 10 + "columns": { 11 + "id": { 12 + "name": "id", 13 + "type": "text", 14 + "primaryKey": true, 15 + "notNull": true 16 + }, 17 + "task_id": { 18 + "name": "task_id", 19 + "type": "text", 20 + "primaryKey": false, 21 + "notNull": true 22 + }, 23 + "type": { 24 + "name": "type", 25 + "type": "text", 26 + "primaryKey": false, 27 + "notNull": true 28 + }, 29 + "created_at": { 30 + "name": "created_at", 31 + "type": "timestamp", 32 + "primaryKey": false, 33 + "notNull": true, 34 + "default": "now()" 35 + }, 36 + "user_email": { 37 + "name": "user_email", 38 + "type": "text", 39 + "primaryKey": false, 40 + "notNull": true 41 + }, 42 + "content": { 43 + "name": "content", 44 + "type": "text", 45 + "primaryKey": false, 46 + "notNull": false 47 + } 48 + }, 49 + "indexes": {}, 50 + "foreignKeys": { 51 + "activity_task_id_task_id_fk": { 52 + "name": "activity_task_id_task_id_fk", 53 + "tableFrom": "activity", 54 + "tableTo": "task", 55 + "columnsFrom": ["task_id"], 56 + "columnsTo": ["id"], 57 + "onDelete": "cascade", 58 + "onUpdate": "cascade" 59 + }, 60 + "activity_user_email_user_email_fk": { 61 + "name": "activity_user_email_user_email_fk", 62 + "tableFrom": "activity", 63 + "tableTo": "user", 64 + "columnsFrom": ["user_email"], 65 + "columnsTo": ["email"], 66 + "onDelete": "cascade", 67 + "onUpdate": "cascade" 68 + } 69 + }, 70 + "compositePrimaryKeys": {}, 71 + "uniqueConstraints": {}, 72 + "policies": {}, 73 + "checkConstraints": {}, 74 + "isRLSEnabled": false 75 + }, 76 + "public.github_integration": { 77 + "name": "github_integration", 78 + "schema": "", 79 + "columns": { 80 + "id": { 81 + "name": "id", 82 + "type": "text", 83 + "primaryKey": true, 84 + "notNull": true 85 + }, 86 + "project_id": { 87 + "name": "project_id", 88 + "type": "text", 89 + "primaryKey": false, 90 + "notNull": true 91 + }, 92 + "repository_owner": { 93 + "name": "repository_owner", 94 + "type": "text", 95 + "primaryKey": false, 96 + "notNull": true 97 + }, 98 + "repository_name": { 99 + "name": "repository_name", 100 + "type": "text", 101 + "primaryKey": false, 102 + "notNull": true 103 + }, 104 + "installation_id": { 105 + "name": "installation_id", 106 + "type": "integer", 107 + "primaryKey": false, 108 + "notNull": false 109 + }, 110 + "is_active": { 111 + "name": "is_active", 112 + "type": "boolean", 113 + "primaryKey": false, 114 + "notNull": false, 115 + "default": true 116 + }, 117 + "created_at": { 118 + "name": "created_at", 119 + "type": "timestamp", 120 + "primaryKey": false, 121 + "notNull": true, 122 + "default": "now()" 123 + }, 124 + "updated_at": { 125 + "name": "updated_at", 126 + "type": "timestamp", 127 + "primaryKey": false, 128 + "notNull": true, 129 + "default": "now()" 130 + } 131 + }, 132 + "indexes": {}, 133 + "foreignKeys": { 134 + "github_integration_project_id_project_id_fk": { 135 + "name": "github_integration_project_id_project_id_fk", 136 + "tableFrom": "github_integration", 137 + "tableTo": "project", 138 + "columnsFrom": ["project_id"], 139 + "columnsTo": ["id"], 140 + "onDelete": "cascade", 141 + "onUpdate": "cascade" 142 + } 143 + }, 144 + "compositePrimaryKeys": {}, 145 + "uniqueConstraints": { 146 + "github_integration_project_id_unique": { 147 + "name": "github_integration_project_id_unique", 148 + "nullsNotDistinct": false, 149 + "columns": ["project_id"] 150 + } 151 + }, 152 + "policies": {}, 153 + "checkConstraints": {}, 154 + "isRLSEnabled": false 155 + }, 156 + "public.label": { 157 + "name": "label", 158 + "schema": "", 159 + "columns": { 160 + "id": { 161 + "name": "id", 162 + "type": "text", 163 + "primaryKey": true, 164 + "notNull": true 165 + }, 166 + "name": { 167 + "name": "name", 168 + "type": "text", 169 + "primaryKey": false, 170 + "notNull": true 171 + }, 172 + "color": { 173 + "name": "color", 174 + "type": "text", 175 + "primaryKey": false, 176 + "notNull": true 177 + }, 178 + "created_at": { 179 + "name": "created_at", 180 + "type": "timestamp", 181 + "primaryKey": false, 182 + "notNull": true, 183 + "default": "now()" 184 + }, 185 + "task_id": { 186 + "name": "task_id", 187 + "type": "text", 188 + "primaryKey": false, 189 + "notNull": true 190 + } 191 + }, 192 + "indexes": {}, 193 + "foreignKeys": { 194 + "label_task_id_task_id_fk": { 195 + "name": "label_task_id_task_id_fk", 196 + "tableFrom": "label", 197 + "tableTo": "task", 198 + "columnsFrom": ["task_id"], 199 + "columnsTo": ["id"], 200 + "onDelete": "cascade", 201 + "onUpdate": "cascade" 202 + } 203 + }, 204 + "compositePrimaryKeys": {}, 205 + "uniqueConstraints": {}, 206 + "policies": {}, 207 + "checkConstraints": {}, 208 + "isRLSEnabled": false 209 + }, 210 + "public.notification": { 211 + "name": "notification", 212 + "schema": "", 213 + "columns": { 214 + "id": { 215 + "name": "id", 216 + "type": "text", 217 + "primaryKey": true, 218 + "notNull": true 219 + }, 220 + "user_email": { 221 + "name": "user_email", 222 + "type": "text", 223 + "primaryKey": false, 224 + "notNull": true 225 + }, 226 + "title": { 227 + "name": "title", 228 + "type": "text", 229 + "primaryKey": false, 230 + "notNull": true 231 + }, 232 + "content": { 233 + "name": "content", 234 + "type": "text", 235 + "primaryKey": false, 236 + "notNull": false 237 + }, 238 + "type": { 239 + "name": "type", 240 + "type": "text", 241 + "primaryKey": false, 242 + "notNull": true, 243 + "default": "'info'" 244 + }, 245 + "is_read": { 246 + "name": "is_read", 247 + "type": "boolean", 248 + "primaryKey": false, 249 + "notNull": false, 250 + "default": false 251 + }, 252 + "resource_id": { 253 + "name": "resource_id", 254 + "type": "text", 255 + "primaryKey": false, 256 + "notNull": false 257 + }, 258 + "resource_type": { 259 + "name": "resource_type", 260 + "type": "text", 261 + "primaryKey": false, 262 + "notNull": false 263 + }, 264 + "created_at": { 265 + "name": "created_at", 266 + "type": "timestamp", 267 + "primaryKey": false, 268 + "notNull": true, 269 + "default": "now()" 270 + } 271 + }, 272 + "indexes": {}, 273 + "foreignKeys": { 274 + "notification_user_email_user_email_fk": { 275 + "name": "notification_user_email_user_email_fk", 276 + "tableFrom": "notification", 277 + "tableTo": "user", 278 + "columnsFrom": ["user_email"], 279 + "columnsTo": ["email"], 280 + "onDelete": "cascade", 281 + "onUpdate": "cascade" 282 + } 283 + }, 284 + "compositePrimaryKeys": {}, 285 + "uniqueConstraints": {}, 286 + "policies": {}, 287 + "checkConstraints": {}, 288 + "isRLSEnabled": false 289 + }, 290 + "public.project": { 291 + "name": "project", 292 + "schema": "", 293 + "columns": { 294 + "id": { 295 + "name": "id", 296 + "type": "text", 297 + "primaryKey": true, 298 + "notNull": true 299 + }, 300 + "workspace_id": { 301 + "name": "workspace_id", 302 + "type": "text", 303 + "primaryKey": false, 304 + "notNull": true 305 + }, 306 + "slug": { 307 + "name": "slug", 308 + "type": "text", 309 + "primaryKey": false, 310 + "notNull": true 311 + }, 312 + "icon": { 313 + "name": "icon", 314 + "type": "text", 315 + "primaryKey": false, 316 + "notNull": false, 317 + "default": "'Layout'" 318 + }, 319 + "name": { 320 + "name": "name", 321 + "type": "text", 322 + "primaryKey": false, 323 + "notNull": true 324 + }, 325 + "description": { 326 + "name": "description", 327 + "type": "text", 328 + "primaryKey": false, 329 + "notNull": false 330 + }, 331 + "created_at": { 332 + "name": "created_at", 333 + "type": "timestamp", 334 + "primaryKey": false, 335 + "notNull": true, 336 + "default": "now()" 337 + }, 338 + "is_public": { 339 + "name": "is_public", 340 + "type": "boolean", 341 + "primaryKey": false, 342 + "notNull": false, 343 + "default": false 344 + } 345 + }, 346 + "indexes": {}, 347 + "foreignKeys": { 348 + "project_workspace_id_workspace_id_fk": { 349 + "name": "project_workspace_id_workspace_id_fk", 350 + "tableFrom": "project", 351 + "tableTo": "workspace", 352 + "columnsFrom": ["workspace_id"], 353 + "columnsTo": ["id"], 354 + "onDelete": "cascade", 355 + "onUpdate": "cascade" 356 + } 357 + }, 358 + "compositePrimaryKeys": {}, 359 + "uniqueConstraints": {}, 360 + "policies": {}, 361 + "checkConstraints": {}, 362 + "isRLSEnabled": false 363 + }, 364 + "public.session": { 365 + "name": "session", 366 + "schema": "", 367 + "columns": { 368 + "id": { 369 + "name": "id", 370 + "type": "text", 371 + "primaryKey": true, 372 + "notNull": true 373 + }, 374 + "user_id": { 375 + "name": "user_id", 376 + "type": "text", 377 + "primaryKey": false, 378 + "notNull": true 379 + }, 380 + "expires_at": { 381 + "name": "expires_at", 382 + "type": "timestamp", 383 + "primaryKey": false, 384 + "notNull": true 385 + } 386 + }, 387 + "indexes": {}, 388 + "foreignKeys": { 389 + "session_user_id_user_id_fk": { 390 + "name": "session_user_id_user_id_fk", 391 + "tableFrom": "session", 392 + "tableTo": "user", 393 + "columnsFrom": ["user_id"], 394 + "columnsTo": ["id"], 395 + "onDelete": "cascade", 396 + "onUpdate": "cascade" 397 + } 398 + }, 399 + "compositePrimaryKeys": {}, 400 + "uniqueConstraints": {}, 401 + "policies": {}, 402 + "checkConstraints": {}, 403 + "isRLSEnabled": false 404 + }, 405 + "public.task": { 406 + "name": "task", 407 + "schema": "", 408 + "columns": { 409 + "id": { 410 + "name": "id", 411 + "type": "text", 412 + "primaryKey": true, 413 + "notNull": true 414 + }, 415 + "project_id": { 416 + "name": "project_id", 417 + "type": "text", 418 + "primaryKey": false, 419 + "notNull": true 420 + }, 421 + "position": { 422 + "name": "position", 423 + "type": "integer", 424 + "primaryKey": false, 425 + "notNull": false, 426 + "default": 0 427 + }, 428 + "number": { 429 + "name": "number", 430 + "type": "integer", 431 + "primaryKey": false, 432 + "notNull": false, 433 + "default": 1 434 + }, 435 + "assignee_email": { 436 + "name": "assignee_email", 437 + "type": "text", 438 + "primaryKey": false, 439 + "notNull": false 440 + }, 441 + "title": { 442 + "name": "title", 443 + "type": "text", 444 + "primaryKey": false, 445 + "notNull": true 446 + }, 447 + "description": { 448 + "name": "description", 449 + "type": "text", 450 + "primaryKey": false, 451 + "notNull": false 452 + }, 453 + "status": { 454 + "name": "status", 455 + "type": "text", 456 + "primaryKey": false, 457 + "notNull": true, 458 + "default": "'to-do'" 459 + }, 460 + "priority": { 461 + "name": "priority", 462 + "type": "text", 463 + "primaryKey": false, 464 + "notNull": false, 465 + "default": "'low'" 466 + }, 467 + "due_date": { 468 + "name": "due_date", 469 + "type": "timestamp", 470 + "primaryKey": false, 471 + "notNull": false 472 + }, 473 + "created_at": { 474 + "name": "created_at", 475 + "type": "timestamp", 476 + "primaryKey": false, 477 + "notNull": true, 478 + "default": "now()" 479 + } 480 + }, 481 + "indexes": {}, 482 + "foreignKeys": { 483 + "task_project_id_project_id_fk": { 484 + "name": "task_project_id_project_id_fk", 485 + "tableFrom": "task", 486 + "tableTo": "project", 487 + "columnsFrom": ["project_id"], 488 + "columnsTo": ["id"], 489 + "onDelete": "cascade", 490 + "onUpdate": "cascade" 491 + }, 492 + "task_assignee_email_user_email_fk": { 493 + "name": "task_assignee_email_user_email_fk", 494 + "tableFrom": "task", 495 + "tableTo": "user", 496 + "columnsFrom": ["assignee_email"], 497 + "columnsTo": ["email"], 498 + "onDelete": "cascade", 499 + "onUpdate": "cascade" 500 + } 501 + }, 502 + "compositePrimaryKeys": {}, 503 + "uniqueConstraints": {}, 504 + "policies": {}, 505 + "checkConstraints": {}, 506 + "isRLSEnabled": false 507 + }, 508 + "public.time_entry": { 509 + "name": "time_entry", 510 + "schema": "", 511 + "columns": { 512 + "id": { 513 + "name": "id", 514 + "type": "text", 515 + "primaryKey": true, 516 + "notNull": true 517 + }, 518 + "task_id": { 519 + "name": "task_id", 520 + "type": "text", 521 + "primaryKey": false, 522 + "notNull": true 523 + }, 524 + "user_email": { 525 + "name": "user_email", 526 + "type": "text", 527 + "primaryKey": false, 528 + "notNull": false 529 + }, 530 + "description": { 531 + "name": "description", 532 + "type": "text", 533 + "primaryKey": false, 534 + "notNull": false 535 + }, 536 + "start_time": { 537 + "name": "start_time", 538 + "type": "timestamp", 539 + "primaryKey": false, 540 + "notNull": true 541 + }, 542 + "end_time": { 543 + "name": "end_time", 544 + "type": "timestamp", 545 + "primaryKey": false, 546 + "notNull": false 547 + }, 548 + "duration": { 549 + "name": "duration", 550 + "type": "integer", 551 + "primaryKey": false, 552 + "notNull": false, 553 + "default": 0 554 + }, 555 + "created_at": { 556 + "name": "created_at", 557 + "type": "timestamp", 558 + "primaryKey": false, 559 + "notNull": true, 560 + "default": "now()" 561 + } 562 + }, 563 + "indexes": {}, 564 + "foreignKeys": { 565 + "time_entry_task_id_task_id_fk": { 566 + "name": "time_entry_task_id_task_id_fk", 567 + "tableFrom": "time_entry", 568 + "tableTo": "task", 569 + "columnsFrom": ["task_id"], 570 + "columnsTo": ["id"], 571 + "onDelete": "cascade", 572 + "onUpdate": "cascade" 573 + }, 574 + "time_entry_user_email_user_email_fk": { 575 + "name": "time_entry_user_email_user_email_fk", 576 + "tableFrom": "time_entry", 577 + "tableTo": "user", 578 + "columnsFrom": ["user_email"], 579 + "columnsTo": ["email"], 580 + "onDelete": "cascade", 581 + "onUpdate": "cascade" 582 + } 583 + }, 584 + "compositePrimaryKeys": {}, 585 + "uniqueConstraints": {}, 586 + "policies": {}, 587 + "checkConstraints": {}, 588 + "isRLSEnabled": false 589 + }, 590 + "public.user": { 591 + "name": "user", 592 + "schema": "", 593 + "columns": { 594 + "id": { 595 + "name": "id", 596 + "type": "text", 597 + "primaryKey": true, 598 + "notNull": true 599 + }, 600 + "name": { 601 + "name": "name", 602 + "type": "text", 603 + "primaryKey": false, 604 + "notNull": true 605 + }, 606 + "password": { 607 + "name": "password", 608 + "type": "text", 609 + "primaryKey": false, 610 + "notNull": true 611 + }, 612 + "email": { 613 + "name": "email", 614 + "type": "text", 615 + "primaryKey": false, 616 + "notNull": true 617 + }, 618 + "created_at": { 619 + "name": "created_at", 620 + "type": "timestamp", 621 + "primaryKey": false, 622 + "notNull": true, 623 + "default": "now()" 624 + } 625 + }, 626 + "indexes": {}, 627 + "foreignKeys": {}, 628 + "compositePrimaryKeys": {}, 629 + "uniqueConstraints": { 630 + "user_email_unique": { 631 + "name": "user_email_unique", 632 + "nullsNotDistinct": false, 633 + "columns": ["email"] 634 + } 635 + }, 636 + "policies": {}, 637 + "checkConstraints": {}, 638 + "isRLSEnabled": false 639 + }, 640 + "public.workspace": { 641 + "name": "workspace", 642 + "schema": "", 643 + "columns": { 644 + "id": { 645 + "name": "id", 646 + "type": "text", 647 + "primaryKey": true, 648 + "notNull": true 649 + }, 650 + "name": { 651 + "name": "name", 652 + "type": "text", 653 + "primaryKey": false, 654 + "notNull": true 655 + }, 656 + "description": { 657 + "name": "description", 658 + "type": "text", 659 + "primaryKey": false, 660 + "notNull": false 661 + }, 662 + "owner_email": { 663 + "name": "owner_email", 664 + "type": "text", 665 + "primaryKey": false, 666 + "notNull": true 667 + }, 668 + "created_at": { 669 + "name": "created_at", 670 + "type": "timestamp", 671 + "primaryKey": false, 672 + "notNull": true, 673 + "default": "now()" 674 + } 675 + }, 676 + "indexes": {}, 677 + "foreignKeys": { 678 + "workspace_owner_email_user_email_fk": { 679 + "name": "workspace_owner_email_user_email_fk", 680 + "tableFrom": "workspace", 681 + "tableTo": "user", 682 + "columnsFrom": ["owner_email"], 683 + "columnsTo": ["email"], 684 + "onDelete": "cascade", 685 + "onUpdate": "cascade" 686 + } 687 + }, 688 + "compositePrimaryKeys": {}, 689 + "uniqueConstraints": {}, 690 + "policies": {}, 691 + "checkConstraints": {}, 692 + "isRLSEnabled": false 693 + }, 694 + "public.workspace_member": { 695 + "name": "workspace_member", 696 + "schema": "", 697 + "columns": { 698 + "id": { 699 + "name": "id", 700 + "type": "text", 701 + "primaryKey": true, 702 + "notNull": true 703 + }, 704 + "workspace_id": { 705 + "name": "workspace_id", 706 + "type": "text", 707 + "primaryKey": false, 708 + "notNull": false 709 + }, 710 + "user_email": { 711 + "name": "user_email", 712 + "type": "text", 713 + "primaryKey": false, 714 + "notNull": true 715 + }, 716 + "role": { 717 + "name": "role", 718 + "type": "text", 719 + "primaryKey": false, 720 + "notNull": true, 721 + "default": "'member'" 722 + }, 723 + "joined_at": { 724 + "name": "joined_at", 725 + "type": "timestamp", 726 + "primaryKey": false, 727 + "notNull": true, 728 + "default": "now()" 729 + }, 730 + "status": { 731 + "name": "status", 732 + "type": "text", 733 + "primaryKey": false, 734 + "notNull": true, 735 + "default": "'pending'" 736 + } 737 + }, 738 + "indexes": {}, 739 + "foreignKeys": { 740 + "workspace_member_workspace_id_workspace_id_fk": { 741 + "name": "workspace_member_workspace_id_workspace_id_fk", 742 + "tableFrom": "workspace_member", 743 + "tableTo": "workspace", 744 + "columnsFrom": ["workspace_id"], 745 + "columnsTo": ["id"], 746 + "onDelete": "cascade", 747 + "onUpdate": "cascade" 748 + } 749 + }, 750 + "compositePrimaryKeys": {}, 751 + "uniqueConstraints": {}, 752 + "policies": {}, 753 + "checkConstraints": {}, 754 + "isRLSEnabled": false 755 + } 756 + }, 757 + "enums": {}, 758 + "schemas": {}, 759 + "sequences": {}, 760 + "roles": {}, 761 + "policies": {}, 762 + "views": {}, 763 + "_meta": { 764 + "columns": {}, 765 + "schemas": {}, 766 + "tables": {} 767 + } 768 + }
+7
apps/api/drizzle/meta/_journal.json
··· 8 8 "when": 1750083630484, 9 9 "tag": "0000_confused_pixie", 10 10 "breakpoints": true 11 + }, 12 + { 13 + "idx": 1, 14 + "version": "7", 15 + "when": 1750504871903, 16 + "tag": "0001_natural_robin_chapel", 17 + "breakpoints": true 11 18 } 12 19 ] 13 20 }
+4 -2
apps/api/package.json
··· 1 1 { 2 2 "name": "@kaneo/api", 3 - "type": "commonjs", 3 + "type": "module", 4 4 "main": "./src/index.ts", 5 5 "scripts": { 6 6 "dev": "tsx watch src/index.ts", 7 - "build": "esbuild src/index.ts --bundle --platform=node --outdir=dist --format=cjs --external:pg --external:bcrypt --external:mock-aws-s3 --external:aws-sdk --external:nock" 7 + "build": "esbuild src/index.ts --bundle --platform=node --outdir=dist --format=esm --packages=external --external:fs --external:path --external:crypto --external:os --external:util --external:stream --external:buffer --external:events --external:url --external:querystring --external:http --external:https --external:net --external:tls --external:zlib" 8 8 }, 9 9 "dependencies": { 10 10 "@hono/node-server": "^1.14.4", 11 11 "@hono/zod-validator": "^0.5.0", 12 + "@octokit/webhooks": "^14.0.2", 12 13 "@oslojs/crypto": "^1.0.1", 13 14 "@oslojs/encoding": "^1.1.0", 14 15 "@paralleldrive/cuid2": "^2.2.2", ··· 17 18 "drizzle-kit": "^0.31.1", 18 19 "drizzle-orm": "^0.43.0", 19 20 "hono": "^4.7.11", 21 + "octokit": "^5.0.3", 20 22 "pg": "^8.16.0", 21 23 "zod": "^3.25.56" 22 24 },
+19
apps/api/src/database/schema.ts
··· 174 174 resourceType: text("resource_type"), 175 175 createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(), 176 176 }); 177 + 178 + export const githubIntegrationTable = pgTable("github_integration", { 179 + id: text("id") 180 + .$defaultFn(() => createId()) 181 + .primaryKey(), 182 + projectId: text("project_id") 183 + .notNull() 184 + .references(() => projectTable.id, { 185 + onDelete: "cascade", 186 + onUpdate: "cascade", 187 + }) 188 + .unique(), 189 + repositoryOwner: text("repository_owner").notNull(), 190 + repositoryName: text("repository_name").notNull(), 191 + installationId: integer("installation_id"), 192 + isActive: boolean("is_active").default(true), 193 + createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(), 194 + updatedAt: timestamp("updated_at", { mode: "date" }).defaultNow().notNull(), 195 + });
+70
apps/api/src/github-integration/controllers/create-github-integration.ts
··· 1 + import { eq } from "drizzle-orm"; 2 + import { HTTPException } from "hono/http-exception"; 3 + import db from "../../database"; 4 + import { githubIntegrationTable, projectTable } from "../../database/schema"; 5 + import githubApp from "../utils/create-github-app"; 6 + 7 + async function createGithubIntegration({ 8 + projectId, 9 + repositoryOwner, 10 + repositoryName, 11 + }: { 12 + projectId: string; 13 + repositoryOwner: string; 14 + repositoryName: string; 15 + }) { 16 + const project = await db.query.projectTable.findFirst({ 17 + where: eq(projectTable.id, projectId), 18 + }); 19 + 20 + if (!project) { 21 + throw new HTTPException(404, { message: "Project not found" }); 22 + } 23 + 24 + let installationId: number | null = null; 25 + try { 26 + const { data: installation } = 27 + await githubApp.octokit.rest.apps.getRepoInstallation({ 28 + owner: repositoryOwner, 29 + repo: repositoryName, 30 + }); 31 + installationId = installation.id; 32 + } catch (error) { 33 + console.warn("Could not get installation ID for repository:", error); 34 + } 35 + 36 + const existingIntegration = await db.query.githubIntegrationTable.findFirst({ 37 + where: eq(githubIntegrationTable.projectId, projectId), 38 + }); 39 + 40 + if (existingIntegration) { 41 + const [updatedIntegration] = await db 42 + .update(githubIntegrationTable) 43 + .set({ 44 + repositoryOwner, 45 + repositoryName, 46 + installationId, 47 + isActive: true, 48 + updatedAt: new Date(), 49 + }) 50 + .where(eq(githubIntegrationTable.projectId, projectId)) 51 + .returning(); 52 + 53 + return updatedIntegration; 54 + } 55 + 56 + const [newIntegration] = await db 57 + .insert(githubIntegrationTable) 58 + .values({ 59 + projectId, 60 + repositoryOwner, 61 + repositoryName, 62 + installationId, 63 + isActive: true, 64 + }) 65 + .returning(); 66 + 67 + return newIntegration; 68 + } 69 + 70 + export default createGithubIntegration;
+22
apps/api/src/github-integration/controllers/delete-github-integration.ts
··· 1 + import { eq } from "drizzle-orm"; 2 + import { HTTPException } from "hono/http-exception"; 3 + import db from "../../database"; 4 + import { githubIntegrationTable } from "../../database/schema"; 5 + 6 + async function deleteGithubIntegration(projectId: string) { 7 + const existingIntegration = await db.query.githubIntegrationTable.findFirst({ 8 + where: eq(githubIntegrationTable.projectId, projectId), 9 + }); 10 + 11 + if (!existingIntegration) { 12 + throw new HTTPException(404, { message: "GitHub integration not found" }); 13 + } 14 + 15 + await db 16 + .delete(githubIntegrationTable) 17 + .where(eq(githubIntegrationTable.projectId, projectId)); 18 + 19 + return { success: true, message: "GitHub integration deleted" }; 20 + } 21 + 22 + export default deleteGithubIntegration;
+19
apps/api/src/github-integration/controllers/get-github-integration-by-repository-id.ts
··· 1 + import { and, eq } from "drizzle-orm"; 2 + import db from "../../database"; 3 + import { githubIntegrationTable } from "../../database/schema"; 4 + 5 + async function getGithubIntegrationByRepositoryId( 6 + repositoryOwner: string, 7 + repositoryName: string, 8 + ) { 9 + const integration = await db.query.githubIntegrationTable.findFirst({ 10 + where: and( 11 + eq(githubIntegrationTable.repositoryOwner, repositoryOwner), 12 + eq(githubIntegrationTable.repositoryName, repositoryName), 13 + ), 14 + }); 15 + 16 + return integration; 17 + } 18 + 19 + export default getGithubIntegrationByRepositoryId;
+20
apps/api/src/github-integration/controllers/get-github-integration.ts
··· 1 + import { eq } from "drizzle-orm"; 2 + import { HTTPException } from "hono/http-exception"; 3 + import db from "../../database"; 4 + import { githubIntegrationTable } from "../../database/schema"; 5 + 6 + async function getGithubIntegration(projectId: string) { 7 + const integration = await db.query.githubIntegrationTable.findFirst({ 8 + where: eq(githubIntegrationTable.projectId, projectId), 9 + }); 10 + 11 + if (!integration) { 12 + throw new HTTPException(404, { 13 + message: "GitHub integration not found", 14 + }); 15 + } 16 + 17 + return integration; 18 + } 19 + 20 + export default getGithubIntegration;
+114
apps/api/src/github-integration/controllers/list-user-repositories.ts
··· 1 + import { HTTPException } from "hono/http-exception"; 2 + import githubApp from "../utils/create-github-app"; 3 + 4 + async function listUserRepositories() { 5 + try { 6 + const { data: installations } = 7 + await githubApp.octokit.rest.apps.listInstallations(); 8 + 9 + const installationsWithRepos = await Promise.all( 10 + installations.map(async (installation) => { 11 + try { 12 + const installationOctokit = await githubApp.getInstallationOctokit( 13 + installation.id, 14 + ); 15 + const { data: repos } = 16 + await installationOctokit.rest.apps.listReposAccessibleToInstallation( 17 + { 18 + per_page: 500, 19 + }, 20 + ); 21 + 22 + return { 23 + id: installation.id, 24 + account: installation.account 25 + ? { 26 + login: installation.account.login, 27 + type: installation.account.type, 28 + } 29 + : null, 30 + repositories: repos.repositories.map((repo) => repo.full_name), 31 + }; 32 + } catch (error) { 33 + console.warn( 34 + `Failed to get repositories for installation ${installation.id}:`, 35 + error, 36 + ); 37 + return { 38 + id: installation.id, 39 + account: installation.account 40 + ? { 41 + login: installation.account.login, 42 + type: installation.account.type, 43 + } 44 + : null, 45 + repositories: [], 46 + }; 47 + } 48 + }), 49 + ); 50 + 51 + const allRepositories = []; 52 + 53 + for (const installation of installations) { 54 + try { 55 + const installationOctokit = await githubApp.getInstallationOctokit( 56 + installation.id, 57 + ); 58 + const { data: repos } = 59 + await installationOctokit.rest.apps.listReposAccessibleToInstallation(); 60 + 61 + const mappedRepos = repos.repositories.map((repo) => ({ 62 + id: repo.id, 63 + name: repo.name, 64 + full_name: repo.full_name, 65 + private: repo.private, 66 + owner: { 67 + login: repo.owner.login, 68 + avatar_url: repo.owner.avatar_url, 69 + type: repo.owner.type, 70 + }, 71 + description: repo.description, 72 + html_url: repo.html_url, 73 + permissions: repo.permissions 74 + ? { 75 + admin: repo.permissions.admin, 76 + push: repo.permissions.push, 77 + pull: repo.permissions.pull, 78 + } 79 + : undefined, 80 + updated_at: repo.updated_at || new Date().toISOString(), 81 + })); 82 + 83 + allRepositories.push(...mappedRepos); 84 + } catch (error) { 85 + console.warn( 86 + `Failed to get repositories for installation ${installation.id}:`, 87 + error, 88 + ); 89 + } 90 + } 91 + 92 + const uniqueRepositories = allRepositories 93 + .filter( 94 + (repo, index, self) => 95 + index === self.findIndex((r) => r.id === repo.id), 96 + ) 97 + .sort( 98 + (a, b) => 99 + new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime(), 100 + ); 101 + 102 + return { 103 + repositories: uniqueRepositories, 104 + installations: installationsWithRepos, 105 + }; 106 + } catch (error) { 107 + console.error("Failed to list user repositories:", error); 108 + throw new HTTPException(500, { 109 + message: "Failed to fetch repositories from GitHub", 110 + }); 111 + } 112 + } 113 + 114 + export default listUserRepositories;
+162
apps/api/src/github-integration/controllers/verify-github-installation.ts
··· 1 + import { HTTPException } from "hono/http-exception"; 2 + import githubApp from "../utils/create-github-app"; 3 + 4 + type VerificationResult = { 5 + isInstalled: boolean; 6 + installationId: number | null; 7 + repositoryExists: boolean; 8 + repositoryPrivate: boolean | null; 9 + permissions: Record<string, string> | null; 10 + message: string; 11 + installationUrl?: string; 12 + settingsUrl?: string; 13 + hasRequiredPermissions?: boolean; 14 + missingPermissions?: string[]; 15 + }; 16 + 17 + async function verifyGithubInstallation({ 18 + repositoryOwner, 19 + repositoryName, 20 + }: { 21 + repositoryOwner: string; 22 + repositoryName: string; 23 + }): Promise<VerificationResult> { 24 + try { 25 + const { data: installation } = 26 + await githubApp.octokit.rest.apps.getRepoInstallation({ 27 + owner: repositoryOwner, 28 + repo: repositoryName, 29 + }); 30 + 31 + const octokit = await githubApp.getInstallationOctokit(installation.id); 32 + const { data: repo } = await octokit.rest.repos.get({ 33 + owner: repositoryOwner, 34 + repo: repositoryName, 35 + }); 36 + 37 + const requiredPermissions = ["issues"]; 38 + const hasRequiredPermissions = checkPermissions( 39 + installation.permissions, 40 + requiredPermissions, 41 + ); 42 + const missingPermissions = getMissingPermissions( 43 + installation.permissions, 44 + requiredPermissions, 45 + ); 46 + 47 + if (!hasRequiredPermissions) { 48 + return { 49 + isInstalled: true, 50 + installationId: installation.id, 51 + repositoryExists: true, 52 + repositoryPrivate: repo.private, 53 + permissions: installation.permissions, 54 + hasRequiredPermissions: false, 55 + missingPermissions, 56 + message: `GitHub App is installed but missing required permissions: ${missingPermissions.join(", ")}`, 57 + settingsUrl: `https://github.com/settings/installations/${installation.id}`, 58 + installationUrl: process.env.GITHUB_APP_NAME 59 + ? `https://github.com/apps/${process.env.GITHUB_APP_NAME}/installations/new/permissions?target_id=${repo.id}` 60 + : undefined, 61 + }; 62 + } 63 + 64 + return { 65 + isInstalled: true, 66 + installationId: installation.id, 67 + repositoryExists: true, 68 + repositoryPrivate: repo.private, 69 + permissions: installation.permissions, 70 + hasRequiredPermissions: true, 71 + message: 72 + "GitHub App is properly installed and has all required permissions", 73 + settingsUrl: `https://github.com/settings/installations/${installation.id}`, 74 + }; 75 + } catch (error) { 76 + const githubError = error as { status?: number; message?: string }; 77 + 78 + if (githubError.status === 404) { 79 + try { 80 + await githubApp.octokit.rest.repos.get({ 81 + owner: repositoryOwner, 82 + repo: repositoryName, 83 + }); 84 + 85 + const repoId = await getRepositoryId(repositoryOwner, repositoryName); 86 + 87 + return { 88 + isInstalled: false, 89 + installationId: null, 90 + repositoryExists: true, 91 + repositoryPrivate: null, 92 + permissions: null, 93 + hasRequiredPermissions: false, 94 + message: "Repository exists but GitHub App is not installed", 95 + installationUrl: process.env.GITHUB_APP_NAME 96 + ? `https://github.com/apps/${process.env.GITHUB_APP_NAME}/installations/new/permissions?target_id=${repoId}` 97 + : undefined, 98 + settingsUrl: process.env.GITHUB_APP_NAME 99 + ? `https://github.com/apps/${process.env.GITHUB_APP_NAME}` 100 + : undefined, 101 + }; 102 + } catch (repoError) { 103 + const repoGithubError = repoError as { status?: number }; 104 + 105 + if (repoGithubError.status === 404) { 106 + return { 107 + isInstalled: false, 108 + installationId: null, 109 + repositoryExists: false, 110 + repositoryPrivate: null, 111 + permissions: null, 112 + hasRequiredPermissions: false, 113 + message: "Repository does not exist or is not accessible", 114 + }; 115 + } 116 + throw repoError; 117 + } 118 + } 119 + 120 + throw new HTTPException(500, { 121 + message: `Failed to verify GitHub installation: ${githubError.message || "Unknown error"}`, 122 + }); 123 + } 124 + } 125 + 126 + function checkPermissions( 127 + permissions: Record<string, string> | undefined, 128 + required: string[], 129 + ): boolean { 130 + if (!permissions) return false; 131 + 132 + return required.every((perm) => { 133 + const permissionLevel = permissions[perm]; 134 + return permissionLevel === "write" || permissionLevel === "admin"; 135 + }); 136 + } 137 + 138 + function getMissingPermissions( 139 + permissions: Record<string, string> | undefined, 140 + required: string[], 141 + ): string[] { 142 + if (!permissions) return required; 143 + 144 + return required.filter((perm) => { 145 + const permissionLevel = permissions[perm]; 146 + return permissionLevel !== "write" && permissionLevel !== "admin"; 147 + }); 148 + } 149 + 150 + async function getRepositoryId(owner: string, repo: string): Promise<number> { 151 + try { 152 + const { data } = await githubApp.octokit.rest.repos.get({ 153 + owner, 154 + repo, 155 + }); 156 + return data.id; 157 + } catch { 158 + return 0; 159 + } 160 + } 161 + 162 + export default verifyGithubInstallation;
+129
apps/api/src/github-integration/index.ts
··· 1 + import { zValidator } from "@hono/zod-validator"; 2 + import { Hono } from "hono"; 3 + import { z } from "zod"; 4 + import { subscribeToEvent } from "../events"; 5 + import createGithubIntegration from "./controllers/create-github-integration"; 6 + import deleteGithubIntegration from "./controllers/delete-github-integration"; 7 + import getGithubIntegration from "./controllers/get-github-integration"; 8 + import listUserRepositories from "./controllers/list-user-repositories"; 9 + import verifyGithubInstallation from "./controllers/verify-github-installation"; 10 + import githubApp from "./utils/create-github-app"; 11 + import { handleIssueOpened } from "./utils/issue-opened"; 12 + import { handleTaskCreated } from "./utils/task-created"; 13 + 14 + githubApp.webhooks.on("issues.opened", handleIssueOpened); 15 + 16 + subscribeToEvent<{ 17 + taskId: string; 18 + userEmail: string; 19 + title: string; 20 + description: string; 21 + priority: string; 22 + status: string; 23 + number: number; 24 + projectId: string; 25 + }>("task.created", handleTaskCreated); 26 + 27 + const githubIntegration = new Hono() 28 + .get("/app-info", async (c) => { 29 + return c.json({ 30 + appName: process.env.GITHUB_APP_NAME || null, 31 + }); 32 + }) 33 + .get("/repositories", async (c) => { 34 + const repositories = await listUserRepositories(); 35 + return c.json(repositories); 36 + }) 37 + .post( 38 + "/verify", 39 + zValidator( 40 + "json", 41 + z.object({ 42 + repositoryOwner: z.string().min(1), 43 + repositoryName: z.string().min(1), 44 + }), 45 + ), 46 + async (c) => { 47 + const { repositoryOwner, repositoryName } = c.req.valid("json"); 48 + 49 + const verification = await verifyGithubInstallation({ 50 + repositoryOwner, 51 + repositoryName, 52 + }); 53 + 54 + return c.json(verification); 55 + }, 56 + ) 57 + .get( 58 + "/project/:projectId", 59 + zValidator("param", z.object({ projectId: z.string() })), 60 + async (c) => { 61 + const { projectId } = c.req.valid("param"); 62 + const integration = await getGithubIntegration(projectId); 63 + return c.json(integration); 64 + }, 65 + ) 66 + .post( 67 + "/project/:projectId", 68 + zValidator("param", z.object({ projectId: z.string() })), 69 + zValidator( 70 + "json", 71 + z.object({ 72 + repositoryOwner: z.string().min(1), 73 + repositoryName: z.string().min(1), 74 + }), 75 + ), 76 + async (c) => { 77 + const { projectId } = c.req.valid("param"); 78 + const { repositoryOwner, repositoryName } = c.req.valid("json"); 79 + 80 + const integration = await createGithubIntegration({ 81 + projectId, 82 + repositoryOwner, 83 + repositoryName, 84 + }); 85 + 86 + return c.json(integration); 87 + }, 88 + ) 89 + .delete( 90 + "/project/:projectId", 91 + zValidator("param", z.object({ projectId: z.string() })), 92 + async (c) => { 93 + const { projectId } = c.req.valid("param"); 94 + const result = await deleteGithubIntegration(projectId); 95 + return c.json(result); 96 + }, 97 + ) 98 + .post("/webhook", async (c) => { 99 + try { 100 + const arrayBuffer = await c.req.arrayBuffer(); 101 + const body = Buffer.from(arrayBuffer).toString("utf8"); 102 + 103 + const signature = c.req.header("x-hub-signature-256"); 104 + if (!signature) { 105 + return c.json({ error: "Missing signature" }, 400); 106 + } 107 + 108 + const eventName = c.req.header("x-github-event"); 109 + if (!eventName) { 110 + return c.json({ error: "Missing event name" }, 400); 111 + } 112 + 113 + const deliveryId = c.req.header("x-github-delivery") || ""; 114 + 115 + await githubApp.webhooks.verifyAndReceive({ 116 + id: deliveryId, 117 + name: eventName as "issues" | "pull_request" | "push" | string, 118 + signature, 119 + payload: body, 120 + }); 121 + 122 + return c.json({ status: "success" }); 123 + } catch (error) { 124 + console.error("Webhook processing error:", error); 125 + return c.json({ error: "Webhook processing failed" }, 400); 126 + } 127 + }); 128 + 129 + export default githubIntegration;
+25
apps/api/src/github-integration/utils/create-github-app.ts
··· 1 + import dotenv from "dotenv"; 2 + import { App } from "octokit"; 3 + 4 + dotenv.config(); 5 + 6 + if ( 7 + !process.env.GITHUB_APP_ID || 8 + !process.env.GITHUB_PRIVATE_KEY || 9 + !process.env.GITHUB_WEBHOOK_SECRET 10 + ) { 11 + throw new Error( 12 + "GITHUB_APP_ID, GITHUB_PRIVATE_KEY and GITHUB_WEBHOOK_SECRET must be set", 13 + ); 14 + } 15 + 16 + const githubApp: App = new App({ 17 + appId: process.env.GITHUB_APP_ID, 18 + privateKey: process.env.GITHUB_PRIVATE_KEY, 19 + 20 + webhooks: { 21 + secret: process.env.GITHUB_WEBHOOK_SECRET, 22 + }, 23 + }); 24 + 25 + export default githubApp;
+17
apps/api/src/github-integration/utils/extract-issue-priority.ts
··· 1 + type GitHubLabel = string | { name?: string }; 2 + 3 + export function extractIssuePriority( 4 + labels: GitHubLabel[] | undefined, 5 + ): string { 6 + if (!labels) return "medium"; 7 + 8 + const priorityLabels = labels 9 + .map((label) => (typeof label === "string" ? label : label?.name)) 10 + .filter((name) => name?.startsWith("priority:")); 11 + 12 + const firstPriorityLabel = priorityLabels[0]; 13 + const priority = firstPriorityLabel?.replace("priority:", "") || "medium"; 14 + 15 + const validPriorities = ["low", "medium", "high", "urgent"]; 16 + return validPriorities.includes(priority) ? priority : "medium"; 17 + }
+23
apps/api/src/github-integration/utils/format-github-comment.ts
··· 1 + export function formatGitHubComment({ 2 + id, 3 + priority, 4 + status, 5 + title, 6 + }: { 7 + id: string; 8 + priority: string; 9 + status: string; 10 + title: string; 11 + }) { 12 + return `🎯 **Task created** - ${title} 13 + <details> 14 + <summary>Task Details</summary> 15 + 16 + - **Task ID:** ${id} 17 + - **Priority:** ${priority} 18 + - **Status:** ${status} 19 + 20 + 21 + *This issue is automatically synchronized with your Kaneo project.* 22 + </details>`; 23 + }
+11
apps/api/src/github-integration/utils/format-task-description.ts
··· 1 + export function formatTaskDescription(payload: { 2 + number: number; 3 + body: string | null; 4 + html_url: string; 5 + user: { login: string } | null; 6 + repository: { 7 + full_name: string; 8 + }; 9 + }): string { 10 + return `${payload.body || "No description provided"}`; 11 + }
+76
apps/api/src/github-integration/utils/issue-opened.ts
··· 1 + import type { 2 + EmitterWebhookEvent, 3 + EmitterWebhookEventName, 4 + } from "@octokit/webhooks"; 5 + import type { Octokit } from "octokit"; 6 + import createTask from "../../task/controllers/create-task"; 7 + import getGithubIntegrationByRepositoryId from "../controllers/get-github-integration-by-repository-id"; 8 + import { extractIssuePriority } from "./extract-issue-priority"; 9 + import { formatGitHubComment } from "./format-github-comment"; 10 + import { formatTaskDescription } from "./format-task-description"; 11 + 12 + export type HandlerFunction< 13 + TName extends EmitterWebhookEventName, 14 + TTransformed = unknown, 15 + > = (event: EmitterWebhookEvent<TName> & TTransformed) => void; 16 + 17 + export const handleIssueOpened: HandlerFunction< 18 + "issues.opened", 19 + { octokit: Octokit } 20 + > = async ({ payload, octokit }): Promise<void> => { 21 + try { 22 + if (payload.issue.title.startsWith("[Kaneo]")) { 23 + console.log("Skipping Kaneo-created issue to avoid loop"); 24 + return; 25 + } 26 + 27 + const integration = await getGithubIntegrationByRepositoryId( 28 + payload.repository.owner.login, 29 + payload.repository.name, 30 + ); 31 + 32 + if (!integration || !integration.isActive) { 33 + console.log( 34 + "No active Kaneo integration found for repository:", 35 + payload.repository.full_name, 36 + ); 37 + return; 38 + } 39 + 40 + const taskPriority = extractIssuePriority(payload.issue.labels); 41 + 42 + const task = await createTask({ 43 + projectId: integration.projectId, 44 + title: payload.issue.title, 45 + description: formatTaskDescription({ 46 + number: payload.issue.number, 47 + body: payload.issue.body, 48 + html_url: payload.issue.html_url, 49 + user: payload.issue.user, 50 + repository: payload.repository, 51 + }), 52 + status: "to-do", 53 + priority: taskPriority, 54 + dueDate: new Date(), 55 + userEmail: undefined, 56 + }); 57 + 58 + try { 59 + await octokit.rest.issues.createComment({ 60 + owner: payload.repository.owner.login, 61 + repo: payload.repository.name, 62 + issue_number: payload.issue.number, 63 + body: formatGitHubComment({ 64 + id: task.id, 65 + title: payload.issue.title, 66 + priority: task.priority || "medium", 67 + status: task.status || "to-do", 68 + }), 69 + }); 70 + } catch (commentError) { 71 + console.error("Failed to add comment to GitHub issue:", commentError); 72 + } 73 + } catch (error) { 74 + console.error("Failed to create Kaneo task from GitHub issue:", error); 75 + } 76 + };
+74
apps/api/src/github-integration/utils/task-created.ts
··· 1 + import getGithubIntegration from "../controllers/get-github-integration"; 2 + import githubApp from "./create-github-app"; 3 + 4 + export async function handleTaskCreated(data: { 5 + taskId: string; 6 + userEmail: string; 7 + title: string; 8 + description: string; 9 + priority: string; 10 + status: string; 11 + number: number; 12 + projectId: string; 13 + }) { 14 + const { 15 + taskId, 16 + userEmail, 17 + title, 18 + description, 19 + priority, 20 + status, 21 + number, 22 + projectId, 23 + } = data; 24 + 25 + try { 26 + const integration = await getGithubIntegration(projectId); 27 + 28 + if (!integration || !integration.isActive) { 29 + console.log("No active GitHub integration found for project:", projectId); 30 + return; 31 + } 32 + 33 + const { repositoryOwner, repositoryName } = integration; 34 + console.log( 35 + "Creating GitHub issue for repository:", 36 + `${repositoryOwner}/${repositoryName}`, 37 + ); 38 + 39 + let installationId = integration.installationId; 40 + 41 + if (!installationId) { 42 + const { data: installation } = 43 + await githubApp.octokit.rest.apps.getRepoInstallation({ 44 + owner: repositoryOwner, 45 + repo: repositoryName, 46 + }); 47 + installationId = installation.id; 48 + } 49 + 50 + const octokit = await githubApp.getInstallationOctokit(installationId); 51 + 52 + await octokit.rest.issues.create({ 53 + owner: repositoryOwner, 54 + repo: repositoryName, 55 + title: `[Kaneo] ${title}`, 56 + body: `**Task created in Kaneo** 57 + 58 + **Description:** ${description || "No description provided"} 59 + 60 + **Details:** 61 + - Task ID: ${taskId} 62 + - Status: ${status} 63 + - Priority: ${priority || "Not set"} 64 + - Task Number: #${number} 65 + - Assigned to: ${userEmail || "Unassigned"} 66 + 67 + --- 68 + *This issue was automatically created from Kaneo task management system.*`, 69 + labels: ["kaneo", `priority:${priority || "low"}`, `status:${status}`], 70 + }); 71 + } catch (error) { 72 + console.error("Failed to create GitHub issue:", error); 73 + } 74 + }
+8 -1
apps/api/src/index.ts
··· 5 5 import { cors } from "hono/cors"; 6 6 import activity from "./activity"; 7 7 import db from "./database"; 8 + import githubIntegration from "./github-integration"; 8 9 import label from "./label"; 9 10 import { auth } from "./middlewares/auth"; 10 11 import notification from "./notification"; ··· 28 29 credentials: true, 29 30 origin: (origin) => origin || "*", 30 31 }), 32 + ); 33 + 34 + const githubIntegrationRoute = app.route( 35 + "/github-integration", 36 + githubIntegration, 31 37 ); 32 38 33 39 const publicProjectRoute = app.get("/public-project/:id", async (c) => { ··· 120 126 | typeof timeEntryRoute 121 127 | typeof labelRoute 122 128 | typeof notificationRoute 123 - | typeof publicProjectRoute; 129 + | typeof publicProjectRoute 130 + | typeof githubIntegrationRoute;
+1
apps/api/src/task/controllers/create-task.ts
··· 50 50 } 51 51 52 52 await publishEvent("task.created", { 53 + ...createdTask, 53 54 taskId: createdTask.id, 54 55 userEmail: createdTask.userEmail ?? "", 55 56 type: "create",
+3 -2
apps/docs/content/docs/index.mdx
··· 155 155 156 156 ## Next Steps 157 157 158 - Now that you have Kaneo running, explore these deployment options for production: 158 + Now that you have Kaneo running, explore these features and deployment options: 159 159 160 + - **[GitHub Integration](/docs/integrations/github)** - Automatically create GitHub issues when tasks are created 160 161 - **[Deploy with Nginx](/docs/deployments/nginx)** - Set up a reverse proxy with SSL 161 162 - **[Deploy with Traefik](/docs/deployments/traefik)** - Modern reverse proxy with automatic HTTPS 162 163 - **[Kubernetes Deployment](/docs/deployments/kubernetes)** - Scalable container orchestration 163 - - **[Core Concepts](/docs/concepts/overview)** - Learn about Kaneo's architecture and features 164 + - **[Terminology](/docs/terminology)** - Learn about Kaneo's core concepts and features 164 165 165 166 ## Getting Help 166 167
+5
apps/docs/content/docs/integrations/github.mdx
··· 1 + --- 2 + title: GitHub Integration 3 + description: Automatically create GitHub issues when tasks are created in Kaneo. Connect your projects to GitHub repositories for seamless issue tracking and project management integration. 4 + icon: Github 5 + ---
+229
apps/docs/content/docs/integrations/github/configuration.mdx
··· 1 + --- 2 + title: Configuration & Setup 3 + description: Configure Kaneo with your GitHub App credentials and connect projects to repositories. Includes Docker Compose and Kubernetes examples. 4 + icon: Cog 5 + --- 6 + 7 + import { Step, Steps } from 'fumadocs-ui/components/steps'; 8 + import { Callout } from 'fumadocs-ui/components/callout'; 9 + 10 + This guide covers configuring Kaneo with your GitHub App credentials and connecting projects to repositories. 11 + 12 + <Callout type="info"> 13 + Make sure you've completed the [GitHub App setup](/docs/integrations/github/setup) before proceeding. 14 + </Callout> 15 + 16 + ## Environment Variables 17 + 18 + Add the following environment variables to your Kaneo deployment: 19 + 20 + ```bash 21 + # GitHub App Configuration 22 + GITHUB_APP_ID=123456 23 + GITHUB_CLIENT_ID=Iv1.abc123def456 24 + GITHUB_CLIENT_SECRET=abc123def456ghi789jkl012mno345pqr678stu 25 + GITHUB_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY----- 26 + MIIEpAIBAAKCAQEA... 27 + [Full contents of your private key] 28 + ... 29 + -----END RSA PRIVATE KEY-----" 30 + GITHUB_WEBHOOK_SECRET=your-webhook-secret-here 31 + 32 + # Optional: GitHub App Name for installation URLs 33 + GITHUB_APP_NAME=kaneo-your-instance-name 34 + ``` 35 + 36 + ### Variable Reference 37 + 38 + | Variable | Description | Required | Example | 39 + |----------|-------------|----------|---------| 40 + | `GITHUB_APP_ID` | Your GitHub App's ID | ✅ | `123456` | 41 + | `GITHUB_CLIENT_ID` | OAuth client ID from your app | ✅ | `Iv1.abc123def456` | 42 + | `GITHUB_CLIENT_SECRET` | OAuth client secret | ✅ | `abc123def456ghi789jkl012mno345pqr678stu` | 43 + | `GITHUB_PRIVATE_KEY` | Full private key content (with newlines) | ✅ | `-----BEGIN RSA...` | 44 + | `GITHUB_WEBHOOK_SECRET` | Secret for webhook verification | ✅ | `your-secret` | 45 + | `GITHUB_APP_NAME` | App name for installation URLs | ⚠️ | `kaneo-mycompany` | 46 + 47 + <Callout type="warning"> 48 + `GITHUB_APP_NAME` is optional but recommended. It's used to generate direct installation links in the UI. 49 + </Callout> 50 + 51 + ## Deployment Examples 52 + 53 + ### Docker Compose 54 + 55 + Update your `compose.yml` file: 56 + 57 + ```yaml 58 + services: 59 + backend: 60 + image: ghcr.io/usekaneo/api:latest 61 + environment: 62 + # ... other environment variables 63 + GITHUB_APP_ID: "123456" 64 + GITHUB_CLIENT_ID: "Iv1.abc123def456" 65 + GITHUB_CLIENT_SECRET: "abc123def456ghi789jkl012mno345pqr678stu" 66 + GITHUB_PRIVATE_KEY: | 67 + -----BEGIN RSA PRIVATE KEY----- 68 + MIIEpAIBAAKCAQEA... 69 + [Full contents of your private key] 70 + ... 71 + -----END RSA PRIVATE KEY----- 72 + GITHUB_WEBHOOK_SECRET: "your-webhook-secret-here" 73 + GITHUB_APP_NAME: "kaneo-mycompany" 74 + # ... rest of configuration 75 + ``` 76 + 77 + <Callout type="tip"> 78 + Use the `|` YAML syntax for multi-line environment variables like the private key. 79 + </Callout> 80 + 81 + ### Kubernetes 82 + 83 + Create a secret for your GitHub credentials: 84 + 85 + ```yaml 86 + apiVersion: v1 87 + kind: Secret 88 + metadata: 89 + name: github-integration 90 + namespace: kaneo 91 + type: Opaque 92 + stringData: 93 + GITHUB_APP_ID: "123456" 94 + GITHUB_CLIENT_ID: "Iv1.abc123def456" 95 + GITHUB_CLIENT_SECRET: "abc123def456ghi789jkl012mno345pqr678stu" 96 + GITHUB_PRIVATE_KEY: | 97 + -----BEGIN RSA PRIVATE KEY----- 98 + MIIEpAIBAAKCAQEA... 99 + [Full contents of your private key] 100 + ... 101 + -----END RSA PRIVATE KEY----- 102 + GITHUB_WEBHOOK_SECRET: "your-webhook-secret-here" 103 + GITHUB_APP_NAME: "kaneo-mycompany" 104 + ``` 105 + 106 + Then reference it in your deployment: 107 + 108 + ```yaml 109 + apiVersion: apps/v1 110 + kind: Deployment 111 + metadata: 112 + name: kaneo-backend 113 + spec: 114 + template: 115 + spec: 116 + containers: 117 + - name: backend 118 + image: ghcr.io/usekaneo/api:latest 119 + envFrom: 120 + - secretRef: 121 + name: github-integration 122 + ``` 123 + 124 + ### Environment File 125 + 126 + For development or simple deployments, create a `.env` file: 127 + 128 + ```bash 129 + # .env file 130 + GITHUB_APP_ID=123456 131 + GITHUB_CLIENT_ID=Iv1.abc123def456 132 + GITHUB_CLIENT_SECRET=abc123def456ghi789jkl012mno345pqr678stu 133 + GITHUB_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY----- 134 + MIIEpAIBAAKCAQEA... 135 + [Full contents of your private key] 136 + ... 137 + -----END RSA PRIVATE KEY-----" 138 + GITHUB_WEBHOOK_SECRET=your-webhook-secret-here 139 + GITHUB_APP_NAME=kaneo-mycompany 140 + ``` 141 + 142 + ## Connecting Repositories 143 + 144 + Once your environment variables are configured and your backend is restarted, you can connect projects to GitHub repositories. 145 + 146 + <Steps> 147 + <Step> 148 + **Navigate to Project Settings** 149 + 150 + 1. Open your Kaneo project 151 + 2. Go to **Project Settings** 152 + 3. Find the **GitHub Integration** section 153 + </Step> 154 + 155 + <Step> 156 + **Connect Repository** 157 + 158 + You have two options to connect a repository: 159 + 160 + **Option A: Browse Repositories** 161 + 1. Click "Browse Repositories" 162 + 2. Select from repositories where your GitHub App is installed 163 + 3. Click on the desired repository 164 + 165 + **Option B: Manual Entry** 166 + 1. Enter the **Repository Owner** (username or organization) 167 + 2. Enter the **Repository Name** 168 + 3. Click "Verify Installation" 169 + </Step> 170 + 171 + <Step> 172 + **Verify and Connect** 173 + 174 + 1. Kaneo will verify that your GitHub App has access 175 + 2. If successful, click "Connect Repository" 176 + 3. You should see a green "Connected" status 177 + 178 + <Callout type="success"> 179 + Once connected, new tasks created in this project will automatically generate GitHub issues! 180 + </Callout> 181 + </Step> 182 + </Steps> 183 + 184 + ## Testing the Integration 185 + 186 + After connecting a repository, test the integration: 187 + 188 + <Steps> 189 + <Step> 190 + **Create a Test Task** 191 + 192 + Create a new task in your connected Kaneo project with: 193 + - A clear title 194 + - A description 195 + - Set priority and status 196 + </Step> 197 + 198 + <Step> 199 + **Check GitHub** 200 + 201 + Navigate to your connected GitHub repository and check if: 202 + - A new issue was created 203 + - The issue title starts with `[Kaneo]` 204 + - The issue has appropriate labels (`kaneo`, `priority:*`, `status:*`) 205 + - The issue body contains task details 206 + </Step> 207 + 208 + </Steps> 209 + 210 + ## Advanced Configuration 211 + 212 + ### Multiple Organizations 213 + If you need to connect repositories from multiple GitHub organizations: 214 + 215 + 1. Install your GitHub App on each organization 216 + 2. Grant appropriate permissions for each organization 217 + 3. Each Kaneo project can connect to any repository where your app is installed 218 + 219 + ### Custom Labels 220 + Currently, Kaneo uses these default labels: 221 + - `kaneo` - All issues created by Kaneo 222 + - `priority:high|medium|low` - Based on task priority 223 + - `status:todo|in-progress|done` - Based on task status 224 + 225 + Future versions will support custom label configurations. 226 + 227 + --- 228 + 229 + **Having issues?** Check our [troubleshooting guide](/docs/integrations/github/troubleshooting) for common problems and solutions.
+5
apps/docs/content/docs/integrations/github/meta.json
··· 1 + { 2 + "title": "GitHub", 3 + "icon": "Github", 4 + "pages": ["setup", "configuration", "troubleshooting"] 5 + }
+197
apps/docs/content/docs/integrations/github/setup.mdx
··· 1 + --- 2 + title: App Setup 3 + description: Learn how to create and configure a GitHub App for Kaneo integration. Step-by-step guide to register your Kaneo instance with GitHub. 4 + icon: Settings 5 + --- 6 + 7 + import { Step, Steps } from 'fumadocs-ui/components/steps'; 8 + import { Callout } from 'fumadocs-ui/components/callout'; 9 + 10 + This guide walks you through creating and configuring a GitHub App to enable the Kaneo integration. **Start here before deploying or configuring your Kaneo instance.** 11 + 12 + ## Overview 13 + 14 + A GitHub App provides secure, granular access to your repositories and allows Kaneo to: 15 + - Create issues automatically when tasks are created 16 + - Access repository metadata for verification 17 + - Receive webhook events for issue synchronization 18 + 19 + <Callout type="info"> 20 + GitHub Apps are more secure than personal access tokens and provide better permission control. 21 + </Callout> 22 + 23 + <Callout type="error"> 24 + **Important**: Complete this GitHub App setup **before** deploying Kaneo. You'll need the app credentials during deployment configuration. 25 + </Callout> 26 + 27 + ## Step 1: Create a GitHub App 28 + 29 + <Steps> 30 + <Step> 31 + **Navigate to GitHub App Settings** 32 + 33 + Go to [GitHub Developer Settings](https://github.com/settings/developers) and click "New GitHub App". 34 + </Step> 35 + 36 + <Step> 37 + **Configure Basic Information** 38 + 39 + Fill in the basic app information: 40 + 41 + - **GitHub App name**: `kaneo-[your-instance-name]` (must be unique across GitHub) 42 + - **Description**: `Kaneo project management integration` 43 + - **Homepage URL**: Your planned Kaneo instance URL (e.g., `https://kaneo.yourdomain.com`) 44 + - **Webhook URL**: `https://your-kaneo-instance.com/github-integration/webhook` 45 + - **Webhook secret**: Generate a secure random string (save this for later) 46 + 47 + <Callout type="warning"> 48 + Replace `your-kaneo-instance.com` with your planned Kaneo domain. The webhook URL must be publicly accessible once deployed. 49 + </Callout> 50 + </Step> 51 + 52 + <Step> 53 + **Set Repository Permissions** 54 + 55 + Configure the following permissions: 56 + 57 + | Permission | Access | Purpose | 58 + |------------|---------|---------| 59 + | **Issues** | Read & Write | Create and update issues | 60 + | **Metadata** | Read | Access repository information | 61 + | **Contents** | Read | Access repository metadata | 62 + 63 + <Callout type="error"> 64 + **Critical**: Make sure to grant **Write** access to Issues - this is required for creating issues automatically. 65 + </Callout> 66 + </Step> 67 + 68 + <Step> 69 + **Subscribe to Webhook Events** 70 + 71 + **This step is required for proper integration.** Subscribe to these events: 72 + 73 + - **Issues** - To track when issues are created, updated, or closed 74 + - **Issue Comments** - To track comments on issues 75 + 76 + <Callout type="error"> 77 + **Required**: You must subscribe to **Issues** events for the integration to work properly. This enables bidirectional synchronization between Kaneo tasks and GitHub issues. 78 + </Callout> 79 + 80 + <Callout type="info"> 81 + These webhook events enable real-time synchronization between your Kaneo tasks and GitHub issues. 82 + </Callout> 83 + </Step> 84 + 85 + <Step> 86 + **Create the App** 87 + 88 + Click "Create GitHub App" to create your app. You'll be redirected to the app settings page. 89 + </Step> 90 + </Steps> 91 + 92 + ## Step 2: Generate Credentials 93 + 94 + <Steps> 95 + <Step> 96 + **Generate and Download Private Key** 97 + 98 + 1. Scroll down to the "Private keys" section 99 + 2. Click "Generate a private key" 100 + 3. Download the `.pem` file - you'll need its contents for configuration 101 + 102 + <Callout type="warn"> 103 + Keep this private key secure! It's used to authenticate your Kaneo instance with GitHub. 104 + </Callout> 105 + </Step> 106 + 107 + <Step> 108 + **Note Your App Information** 109 + 110 + From the app settings page, note down these important values: 111 + 112 + - **App ID** (shown at the top of the page) 113 + - **Client ID** (in the "Basic information" section) 114 + - **Client Secret** (click "Generate a new client secret") 115 + - **Webhook Secret** (the one you created earlier) 116 + - **Private Key** (contents of the downloaded `.pem` file) 117 + 118 + <Callout type="info"> 119 + You'll need all of these values for the [configuration step](/docs/integrations/github/configuration). 120 + </Callout> 121 + </Step> 122 + </Steps> 123 + 124 + ## Step 3: Install the App 125 + 126 + <Steps> 127 + <Step> 128 + **Install on Your Account/Organization** 129 + 130 + 1. Go to your GitHub App settings page 131 + 2. Click "Install App" in the left sidebar 132 + 3. Choose your account or organization 133 + 4. Select repositories: 134 + - **All repositories** (if you want integration available for all repos) 135 + - **Selected repositories** (choose specific repos) 136 + 137 + <Callout type="tip"> 138 + You can modify repository access later by going to your [GitHub App installations](https://github.com/settings/installations). 139 + </Callout> 140 + </Step> 141 + 142 + <Step> 143 + **Verify Installation** 144 + 145 + After installation, you should see your app listed in: 146 + - [Your GitHub App installations](https://github.com/settings/installations) 147 + - The repository's Settings → Integrations & services (for organization repos) 148 + </Step> 149 + </Steps> 150 + 151 + ## Security Best Practices 152 + 153 + ### App Configuration 154 + - **Use descriptive names**: Make it clear the app is for Kaneo integration 155 + - **Limit permissions**: Only grant the minimum required permissions 156 + - **Repository scope**: Install only on repositories that need integration 157 + 158 + ### Credential Management 159 + - **Secure storage**: Store credentials in environment variables or secret management systems 160 + - **Regular rotation**: Generate new private keys periodically 161 + - **Access control**: Limit who has access to the GitHub App settings 162 + 163 + ## Common Issues 164 + 165 + ### App Name Already Taken 166 + If your desired app name is taken, try: 167 + - Adding your organization name: `kaneo-mycompany` 168 + - Adding a timestamp: `kaneo-mycompany-2024` 169 + - Using a different naming convention: `mycompany-kaneo` 170 + 171 + ### Webhook URL Not Accessible 172 + Ensure your Kaneo instance is: 173 + - Publicly accessible on the internet 174 + - Using HTTPS (GitHub requires this for webhooks) 175 + - Not behind authentication that would block GitHub's webhook calls 176 + 177 + ### Permission Issues 178 + Double-check that you've granted: 179 + - **Write** access to Issues (not just Read) 180 + - **Read** access to Metadata and Contents 181 + - Selected the correct repositories during installation 182 + 183 + ## Next Steps 184 + 185 + Now that your GitHub App is created and configured: 186 + 187 + 1. **[Deploy Kaneo](/docs/getting-started/deployment)** - Deploy your Kaneo instance with the GitHub App credentials 188 + 2. **[Configure GitHub Integration](/docs/integrations/github/configuration)** - Complete the integration setup in your deployed Kaneo instance 189 + 3. **[Connect Repositories](/docs/integrations/github/configuration#connecting-repositories)** - Link your projects to GitHub repositories 190 + 191 + <Callout type="info"> 192 + **Important**: Have your GitHub App credentials ready before deployment. You'll need to set them as environment variables during the deployment process. 193 + </Callout> 194 + 195 + --- 196 + 197 + **Need help?** Check our [troubleshooting guide](/docs/integrations/github/troubleshooting) or ask for help on [Discord](https://discord.gg/rU4tSyhXXU).
+296
apps/docs/content/docs/integrations/github/troubleshooting.mdx
··· 1 + --- 2 + title: Troubleshooting 3 + description: Common issues and solutions for GitHub integration. Debug connection problems, permission errors, and webhook issues. 4 + icon: TriangleAlert 5 + --- 6 + 7 + import { Callout } from 'fumadocs-ui/components/callout'; 8 + 9 + This guide helps you diagnose and fix common issues with the GitHub integration. 10 + 11 + ## Common Issues 12 + 13 + ### "GitHub App not installed" Error 14 + 15 + **Problem**: Verification shows the app isn't installed on the repository. 16 + 17 + **Symptoms**: 18 + - Red error message in Kaneo UI 19 + - "Connect Repository" button remains disabled 20 + - Repository doesn't appear in the browser modal 21 + 22 + **Solutions**: 23 + 1. **Check Installation Scope** 24 + - Go to [GitHub App installations](https://github.com/settings/installations) 25 + - Verify your app is installed on the correct account/organization 26 + - Check if the target repository is included in the installation scope 27 + 28 + 2. **Reinstall with Broader Access** 29 + - Edit your app installation 30 + - Change from "Selected repositories" to "All repositories" 31 + - Or add the specific repository to the selected list 32 + 33 + 3. **Verify Repository Ownership** 34 + - Ensure you're entering the correct repository owner (username/organization) 35 + - Check that the repository name is spelled correctly 36 + - Verify the repository exists and you have access to it 37 + 38 + <Callout type="tip"> 39 + Use the "Browse Repositories" feature in Kaneo to see which repositories your app can access. 40 + </Callout> 41 + 42 + ### "Missing Permissions" Error 43 + 44 + **Problem**: App is installed but lacks required permissions. 45 + 46 + **Symptoms**: 47 + - Orange warning in verification 48 + - App appears in installations but can't create issues 49 + - Missing permissions listed in the error message 50 + 51 + **Solutions**: 52 + 1. **Update App Permissions** 53 + - Go to your [GitHub App settings](https://github.com/settings/developers) 54 + - Edit your app and check permissions: 55 + - **Issues**: Must have **Write** access (not just Read) 56 + - **Metadata**: Must have **Read** access 57 + - **Contents**: Must have **Read** access 58 + 59 + 2. **Update Installation** 60 + - After changing app permissions, you may need to update the installation 61 + - Go to [GitHub App installations](https://github.com/settings/installations) 62 + - Click "Configure" next to your app 63 + - Review and accept any permission changes 64 + 65 + 3. **Check Organization Permissions** 66 + - For organization repositories, ensure the organization allows the app 67 + - Organization owners may need to approve third-party apps 68 + 69 + ### "Repository not found" Error 70 + 71 + **Problem**: Repository exists but isn't accessible to the app. 72 + 73 + **Symptoms**: 74 + - Repository not found in verification 75 + - 404-style errors in logs 76 + - Repository doesn't appear in browse modal 77 + 78 + **Solutions**: 79 + 1. **Verify Repository Details** 80 + - Double-check the repository owner and name 81 + - Ensure there are no typos or extra spaces 82 + - Check that the repository is public or your app has access to private repos 83 + 84 + 2. **Check Private Repository Access** 85 + - Private repositories require explicit app installation 86 + - Ensure your GitHub App is installed on the repository owner's account 87 + - Verify the app has permission to access private repositories 88 + 89 + 3. **Organization Repository Issues** 90 + - Check if the organization has third-party app restrictions 91 + - Ensure the app is approved by organization owners 92 + - Verify the app is installed at the organization level, not just personal account 93 + 94 + ### Environment Variable Issues 95 + 96 + **Problem**: Integration doesn't work after configuration. 97 + 98 + **Symptoms**: 99 + - No GitHub issues created when tasks are made 100 + - Authentication errors in logs 101 + - Integration appears configured but doesn't function 102 + 103 + **Solutions**: 104 + 1. **Verify All Variables Are Set** 105 + ```bash 106 + # Check if environment variables are loaded 107 + echo $GITHUB_APP_ID 108 + echo $GITHUB_CLIENT_ID 109 + # etc. 110 + ``` 111 + 112 + 2. **Check Private Key Format** 113 + - Ensure `GITHUB_PRIVATE_KEY` includes the full content with proper line breaks 114 + - The key should start with `-----BEGIN RSA PRIVATE KEY-----` 115 + - The key should end with `-----END RSA PRIVATE KEY-----` 116 + - Include all newlines and formatting from the `.pem` file 117 + 118 + 3. **Restart Backend Service** 119 + - Environment variable changes require a service restart 120 + - For Docker Compose: `docker compose restart backend` 121 + - For Kubernetes: `kubectl rollout restart deployment/kaneo-backend` 122 + 123 + 4. **Check Log Output** 124 + - Look for authentication errors on startup 125 + - Verify the GitHub App ID is recognized 126 + - Check for private key parsing errors 127 + 128 + <Callout type="warning"> 129 + Make sure your private key doesn't have any extra characters or missing newlines. 130 + </Callout> 131 + 132 + ## Webhook Issues 133 + 134 + ### Webhook Not Receiving Events 135 + 136 + **Problem**: GitHub events aren't reaching Kaneo. 137 + 138 + **Symptoms**: 139 + - Webhook events listed in GitHub but no logs in Kaneo 140 + - Webhook delivery failures in GitHub App settings 141 + 142 + **Solutions**: 143 + 1. **Verify Webhook URL** 144 + - Ensure the URL is publicly accessible: `https://your-domain.com/github-integration/webhook` 145 + - Test accessibility from external tools like curl or online checkers 146 + - Verify your domain has proper DNS resolution 147 + 148 + 2. **Check HTTPS Configuration** 149 + - GitHub requires HTTPS for webhook endpoints 150 + - Ensure your SSL certificate is valid and not self-signed 151 + - Test your webhook endpoint with online SSL checkers 152 + 153 + 3. **Firewall and Network Issues** 154 + - Ensure GitHub's IP ranges can reach your server 155 + - Check if your hosting provider blocks incoming webhooks 156 + - Verify no authentication middleware is blocking the webhook endpoint 157 + 158 + ### Webhook Signature Verification Fails 159 + 160 + **Problem**: Webhooks reach Kaneo but signature verification fails. 161 + 162 + **Symptoms**: 163 + - "Invalid signature" errors in logs 164 + - Webhook events rejected with 400 status 165 + 166 + **Solutions**: 167 + 1. **Verify Webhook Secret** 168 + - Ensure `GITHUB_WEBHOOK_SECRET` matches exactly what you set in the GitHub App 169 + - Check for extra spaces or characters 170 + - The secret is case-sensitive 171 + 172 + 2. **Check Secret Configuration** 173 + - In GitHub App settings, verify the webhook secret is set 174 + - If you changed the secret, update the environment variable and restart 175 + - Generate a new secret if there are persistent issues 176 + 177 + ## Testing and Debugging 178 + 179 + ### Testing the Integration 180 + 181 + Follow these steps to systematically test your integration: 182 + 183 + 1. **Verify Environment Variables** 184 + ```bash 185 + # Check that all required variables are set (don't print private key!) 186 + echo "App ID: $GITHUB_APP_ID" 187 + echo "Client ID: $GITHUB_CLIENT_ID" 188 + echo "Webhook Secret: [REDACTED]" 189 + echo "App Name: $GITHUB_APP_NAME" 190 + ``` 191 + 192 + 2. **Test Repository Access** 193 + - Use the "Browse Repositories" feature in Kaneo 194 + - Verify your target repository appears in the list 195 + - Try connecting a test repository first 196 + 197 + 3. **Create a Test Task** 198 + - Create a simple task in your connected project 199 + - Check GitHub immediately for the new issue 200 + - Verify the issue format and labels are correct 201 + 202 + 4. **Check Backend Logs** 203 + Look for these log messages: 204 + ``` 205 + ✅ GitHub issue created: { issueNumber: 42, issueUrl: "...", taskId: "...", repository: "..." } 206 + ❌ Failed to create GitHub issue: [error details] 207 + ✅ Task created: { taskId: "...", title: "...", projectId: "..." } 208 + ``` 209 + 210 + ### Log Analysis 211 + 212 + **Successful Integration Logs**: 213 + ``` 214 + Task created: { taskId: "task_123", title: "Fix bug", projectId: "proj_456" } 215 + Creating GitHub issue for repository: owner/repo 216 + GitHub issue created: { issueNumber: 42, issueUrl: "https://github.com/owner/repo/issues/42" } 217 + ``` 218 + 219 + **Common Error Patterns**: 220 + ``` 221 + ❌ No active GitHub integration found for project: proj_456 222 + ❌ Failed to create GitHub issue: HttpError: Bad credentials 223 + ❌ Repository not found: owner/nonexistent-repo 224 + ❌ App installation not found for repository: owner/repo 225 + ``` 226 + 227 + ### GitHub App Diagnostics 228 + 229 + Use GitHub's built-in tools to diagnose issues: 230 + 231 + 1. **App Installation Page** 232 + - Visit [GitHub App installations](https://github.com/settings/installations) 233 + - Check which repositories your app can access 234 + - Review granted permissions 235 + 236 + 2. **Webhook Delivery Logs** 237 + - In your GitHub App settings, check the "Advanced" tab 238 + - Review recent webhook deliveries and their status codes 239 + - Look for failed deliveries or error responses 240 + 241 + 3. **Repository Integration Status** 242 + - Go to your repository's Settings → Integrations & services 243 + - Verify your app appears in the list 244 + - Check the app's permissions and access 245 + 246 + ## Getting Help 247 + 248 + If you're still experiencing issues: 249 + 250 + ### Before Asking for Help 251 + 252 + 1. **Check Your Setup** 253 + - Verify all environment variables are correct 254 + - Ensure your GitHub App has the right permissions 255 + - Test with a simple repository first 256 + 257 + 2. **Gather Information** 258 + - Note the exact error messages 259 + - Check both Kaneo logs and GitHub webhook delivery logs 260 + - Document your environment (Docker, Kubernetes, etc.) 261 + 262 + 3. **Test Basic Functionality** 263 + - Try creating a task without GitHub integration 264 + - Verify your Kaneo instance is working normally 265 + - Test with different repositories if possible 266 + 267 + ### Support Channels 268 + 269 + - **[Discord Community](https://discord.gg/rU4tSyhXXU)** - Real-time help from the community 270 + - **[GitHub Issues](https://github.com/usekaneo/kaneo/issues)** - Report bugs or request features 271 + - **[Documentation](/docs)** - Review setup guides and troubleshooting 272 + 273 + ### What to Include in Support Requests 274 + 275 + 1. **Environment Details** 276 + - Kaneo version 277 + - Deployment method (Docker, Kubernetes, etc.) 278 + - Operating system 279 + 280 + 2. **Error Information** 281 + - Exact error messages 282 + - Screenshots of the issue 283 + - Relevant log entries 284 + 285 + 3. **Configuration Details** 286 + - Which environment variables you've set (don't share secret values!) 287 + - GitHub App permissions you've granted 288 + - Repository details (without sharing sensitive info) 289 + 290 + <Callout type="info"> 291 + When sharing logs or error messages, always redact sensitive information like private keys, secrets, or access tokens. 292 + </Callout> 293 + 294 + --- 295 + 296 + **Still stuck?** The Kaneo community is here to help! Join our [Discord](https://discord.gg/rU4tSyhXXU) for real-time support.
+6
apps/docs/content/docs/integrations/meta.json
··· 1 + { 2 + "title": "Integrations", 3 + "icon": "Plug", 4 + "pages": ["github"], 5 + "defaultOpen": true 6 + }
+1
apps/web/package.json
··· 57 57 "sonner": "^2.0.3", 58 58 "tailwind-merge": "^3.3.0", 59 59 "tailwindcss-animate": "^1.0.7", 60 + "tiptap-markdown": "^0.8.10", 60 61 "zod": "^3.25.67", 61 62 "zustand": "^5.0.4" 62 63 },
+44 -2
apps/web/src/components/common/editor.tsx
··· 1 1 import { cn } from "@/lib/cn"; 2 2 import Highlight from "@tiptap/extension-highlight"; 3 + import Link from "@tiptap/extension-link"; 3 4 import Placeholder from "@tiptap/extension-placeholder"; 4 5 import TaskItem from "@tiptap/extension-task-item"; 5 6 import TaskList from "@tiptap/extension-task-list"; ··· 16 17 Code, 17 18 HighlighterIcon, 18 19 Italic, 20 + Link2, 19 21 List, 20 22 ListOrdered, 21 23 Quote, ··· 23 25 Type, 24 26 } from "lucide-react"; 25 27 import { useEffect, useRef, useState } from "react"; 28 + import { Markdown } from "tiptap-markdown"; 26 29 27 30 interface EditorProps { 28 31 value: string; ··· 95 98 }, 96 99 }, 97 100 }), 101 + Link.configure({ 102 + openOnClick: true, 103 + autolink: true, 104 + defaultProtocol: "https", 105 + HTMLAttributes: { 106 + class: 107 + "text-indigo-600 dark:text-indigo-400 underline cursor-pointer hover:text-indigo-800 dark:hover:text-indigo-300", 108 + }, 109 + }), 98 110 Highlight.configure({ 99 111 HTMLAttributes: { 100 112 class: ··· 112 124 class: "flex gap-2 items-start", 113 125 }, 114 126 }), 115 - 127 + Markdown.configure({ 128 + html: true, 129 + tightLists: true, 130 + linkify: true, 131 + breaks: true, 132 + transformPastedText: true, 133 + transformCopiedText: true, 134 + }), 116 135 Placeholder.configure({ 117 136 placeholder, 118 137 }), ··· 122 141 }, 123 142 content: value, 124 143 onUpdate: ({ editor }: { editor: TiptapEditor }) => { 125 - onChange(editor.getHTML()); 144 + const markdown = editor.storage.markdown.getMarkdown(); 145 + onChange(markdown); 126 146 }, 127 147 }); 128 148 ··· 136 156 document.addEventListener("mousedown", handleClickOutside); 137 157 return () => document.removeEventListener("mousedown", handleClickOutside); 138 158 }, []); 159 + 160 + useEffect(() => { 161 + if (editor && value !== editor.storage.markdown.getMarkdown()) { 162 + editor.commands.setContent(value); 163 + } 164 + }, [value, editor]); 139 165 140 166 const getCurrentTextStyle = () => { 141 167 if (!editor) return TEXT_OPTIONS[0]; ··· 295 321 )} 296 322 > 297 323 <Code className="w-4 h-4 text-zinc-600 dark:text-zinc-400" /> 324 + </button> 325 + <div className="w-px h-6 mx-1 bg-zinc-200 dark:bg-zinc-800" /> 326 + <button 327 + type="button" 328 + onClick={() => { 329 + const url = window.prompt("Enter URL:"); 330 + if (url) { 331 + editor.chain().focus().setLink({ href: url }).run(); 332 + } 333 + }} 334 + className={cn( 335 + "p-1.5 rounded-lg hover:bg-zinc-100 dark:hover:bg-zinc-800", 336 + editor.isActive("link") && "bg-zinc-100 dark:bg-zinc-800", 337 + )} 338 + > 339 + <Link2 className="w-4 h-4 text-zinc-600 dark:text-zinc-400" /> 298 340 </button> 299 341 </div> 300 342 <EditorContent
+547
apps/web/src/components/project/github-integration-settings.tsx
··· 1 + import { RepositoryBrowserModal } from "@/components/project/repository-browser-modal"; 2 + import { Button } from "@/components/ui/button"; 3 + import { 4 + Form, 5 + FormControl, 6 + FormDescription, 7 + FormField, 8 + FormItem, 9 + FormLabel, 10 + FormMessage, 11 + } from "@/components/ui/form"; 12 + import { Input } from "@/components/ui/input"; 13 + import type { VerifyGithubInstallationResponse } from "@/fetchers/github-integration/verify-github-installation"; 14 + import { 15 + useCreateGithubIntegration, 16 + useDeleteGithubIntegration, 17 + useVerifyGithubInstallation, 18 + } from "@/hooks/mutations/github-integration/use-create-github-integration"; 19 + import useGetGithubIntegration from "@/hooks/queries/github-integration/use-get-github-integration"; 20 + import { cn } from "@/lib/cn"; 21 + import { standardSchemaResolver } from "@hookform/resolvers/standard-schema"; 22 + import { 23 + AlertTriangle, 24 + CheckCircle, 25 + ExternalLink, 26 + GitBranch, 27 + Github, 28 + Link, 29 + RefreshCw, 30 + Unlink, 31 + XCircle, 32 + } from "lucide-react"; 33 + import React from "react"; 34 + import { useForm } from "react-hook-form"; 35 + import { toast } from "sonner"; 36 + import { z } from "zod/v4"; 37 + 38 + const githubIntegrationSchema = z.object({ 39 + repositoryOwner: z 40 + .string() 41 + .min(1, "Repository owner is required") 42 + .regex(/^[a-zA-Z0-9-]+$/, "Invalid repository owner format"), 43 + repositoryName: z 44 + .string() 45 + .min(1, "Repository name is required") 46 + .regex(/^[a-zA-Z0-9._-]+$/, "Invalid repository name format"), 47 + }); 48 + 49 + type GithubIntegrationFormValues = z.infer<typeof githubIntegrationSchema>; 50 + 51 + export function GitHubIntegrationSettings({ 52 + projectId, 53 + }: { projectId: string }) { 54 + const { data: integration, isLoading } = useGetGithubIntegration(projectId); 55 + const { mutateAsync: createIntegration, isPending: isCreating } = 56 + useCreateGithubIntegration(); 57 + const { mutateAsync: deleteIntegration, isPending: isDeleting } = 58 + useDeleteGithubIntegration(); 59 + const { mutateAsync: verifyInstallation, isPending: isVerifying } = 60 + useVerifyGithubInstallation(); 61 + 62 + const [verificationResult, setVerificationResult] = 63 + React.useState<VerifyGithubInstallationResponse | null>(null); 64 + const [showRepositoryBrowser, setShowRepositoryBrowser] = 65 + React.useState(false); 66 + 67 + const form = useForm<GithubIntegrationFormValues>({ 68 + resolver: standardSchemaResolver(githubIntegrationSchema), 69 + defaultValues: { 70 + repositoryOwner: integration?.repositoryOwner || "", 71 + repositoryName: integration?.repositoryName || "", 72 + }, 73 + }); 74 + 75 + React.useEffect(() => { 76 + if (integration) { 77 + form.reset({ 78 + repositoryOwner: integration.repositoryOwner, 79 + repositoryName: integration.repositoryName, 80 + }); 81 + } 82 + }, [integration, form]); 83 + 84 + const repositoryOwner = form.watch("repositoryOwner"); 85 + const repositoryName = form.watch("repositoryName"); 86 + 87 + const handleVerifyInstallation = React.useCallback( 88 + async (data: GithubIntegrationFormValues, showToast = true) => { 89 + try { 90 + const result = await verifyInstallation(data); 91 + setVerificationResult(result); 92 + 93 + if (showToast) { 94 + if (result.isInstalled && result.hasRequiredPermissions) { 95 + toast.success("GitHub App is properly installed!"); 96 + } else if (result.isInstalled) { 97 + toast.warning( 98 + "GitHub App is installed but missing required permissions", 99 + ); 100 + } else if (result.repositoryExists) { 101 + toast.warning( 102 + "GitHub App needs to be installed on this repository", 103 + ); 104 + } else { 105 + toast.error("Repository not found or not accessible"); 106 + } 107 + } 108 + } catch (error) { 109 + if (showToast) { 110 + toast.error( 111 + error instanceof Error 112 + ? error.message 113 + : "Failed to verify GitHub installation", 114 + ); 115 + } 116 + setVerificationResult(null); 117 + } 118 + }, 119 + [verifyInstallation], 120 + ); 121 + 122 + React.useEffect(() => { 123 + if (repositoryOwner && repositoryName && form.formState.isValid) { 124 + handleVerifyInstallation({ repositoryOwner, repositoryName }, false); 125 + } 126 + }, [ 127 + repositoryOwner, 128 + repositoryName, 129 + form.formState.isValid, 130 + handleVerifyInstallation, 131 + ]); 132 + 133 + const handleRepositorySelect = (repository: { 134 + owner: string; 135 + name: string; 136 + }) => { 137 + form.setValue("repositoryOwner", repository.owner, { 138 + shouldValidate: true, 139 + shouldDirty: true, 140 + shouldTouch: true, 141 + }); 142 + form.setValue("repositoryName", repository.name, { 143 + shouldValidate: true, 144 + shouldDirty: true, 145 + shouldTouch: true, 146 + }); 147 + setShowRepositoryBrowser(false); 148 + 149 + setVerificationResult(null); 150 + }; 151 + 152 + const onSubmit = async (data: GithubIntegrationFormValues) => { 153 + try { 154 + const verification = await verifyInstallation(data); 155 + 156 + if (!verification.isInstalled) { 157 + toast.error("Please install the GitHub App on this repository first"); 158 + return; 159 + } 160 + 161 + if (!verification.hasRequiredPermissions) { 162 + toast.error( 163 + `GitHub App is missing required permissions: ${verification.missingPermissions?.join(", ") || "issues"}. Please update the app permissions.`, 164 + ); 165 + return; 166 + } 167 + 168 + await createIntegration({ 169 + projectId, 170 + data, 171 + }); 172 + toast.success("GitHub integration updated successfully"); 173 + } catch (error) { 174 + toast.error( 175 + error instanceof Error 176 + ? error.message 177 + : "Failed to update GitHub integration", 178 + ); 179 + } 180 + }; 181 + 182 + const handleDelete = async () => { 183 + try { 184 + await deleteIntegration(projectId); 185 + form.reset({ repositoryOwner: "", repositoryName: "" }); 186 + setVerificationResult(null); 187 + toast.success("GitHub integration removed successfully"); 188 + } catch (error) { 189 + toast.error( 190 + error instanceof Error 191 + ? error.message 192 + : "Failed to remove GitHub integration", 193 + ); 194 + } 195 + }; 196 + 197 + if (isLoading) { 198 + return ( 199 + <div className="space-y-4"> 200 + <div className="h-4 bg-gray-200 rounded animate-pulse" /> 201 + <div className="h-10 bg-gray-200 rounded animate-pulse" /> 202 + <div className="h-10 bg-gray-200 rounded animate-pulse" /> 203 + </div> 204 + ); 205 + } 206 + 207 + const isConnected = !!integration && integration.isActive; 208 + 209 + return ( 210 + <div className="space-y-6"> 211 + <div className="flex items-start gap-4"> 212 + <div className="p-2 bg-gray-100 dark:bg-gray-800 rounded-lg"> 213 + <Github className="w-6 h-6" /> 214 + </div> 215 + <div className="flex-1"> 216 + <h3 className="text-lg font-semibold">GitHub Integration</h3> 217 + <p className="text-sm text-gray-600 dark:text-gray-400"> 218 + Connect this project to a GitHub repository to automatically create 219 + issues when tasks are created in Kaneo. 220 + </p> 221 + </div> 222 + </div> 223 + 224 + {isConnected && ( 225 + <div className="flex items-center gap-2 p-3 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg"> 226 + <CheckCircle className="w-4 h-4 text-green-600 dark:text-green-400" /> 227 + <span className="text-sm text-green-700 dark:text-green-300"> 228 + Connected to{" "} 229 + <code className="px-1 py-0.5 bg-green-100 dark:bg-green-800 rounded text-xs"> 230 + {integration.repositoryOwner}/{integration.repositoryName} 231 + </code> 232 + </span> 233 + </div> 234 + )} 235 + 236 + {verificationResult && ( 237 + <div 238 + className={cn( 239 + "flex items-start gap-2 p-3 border rounded-lg", 240 + verificationResult.isInstalled && 241 + verificationResult.hasRequiredPermissions 242 + ? "bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800" 243 + : verificationResult.isInstalled 244 + ? "bg-amber-50 dark:bg-amber-900/20 border-amber-200 dark:border-amber-800" 245 + : verificationResult.repositoryExists 246 + ? "bg-amber-50 dark:bg-amber-900/20 border-amber-200 dark:border-amber-800" 247 + : "bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800", 248 + )} 249 + > 250 + {verificationResult.isInstalled && 251 + verificationResult.hasRequiredPermissions ? ( 252 + <CheckCircle className="w-4 h-4 text-green-600 dark:text-green-400 mt-0.5 flex-shrink-0" /> 253 + ) : verificationResult.isInstalled || 254 + verificationResult.repositoryExists ? ( 255 + <AlertTriangle className="w-4 h-4 text-amber-600 dark:text-amber-400 mt-0.5 flex-shrink-0" /> 256 + ) : ( 257 + <XCircle className="w-4 h-4 text-red-600 dark:text-red-400 mt-0.5 flex-shrink-0" /> 258 + )} 259 + <div 260 + className={cn( 261 + "text-sm flex-1", 262 + verificationResult.isInstalled && 263 + verificationResult.hasRequiredPermissions 264 + ? "text-green-700 dark:text-green-300" 265 + : verificationResult.isInstalled || 266 + verificationResult.repositoryExists 267 + ? "text-amber-700 dark:text-amber-300" 268 + : "text-red-700 dark:text-red-300", 269 + )} 270 + > 271 + <p className="font-medium">{verificationResult.message}</p> 272 + 273 + {verificationResult.isInstalled && 274 + !verificationResult.hasRequiredPermissions && 275 + verificationResult.missingPermissions && ( 276 + <div className="mt-2"> 277 + <p className="text-xs mb-2"> 278 + Missing permissions:{" "} 279 + <strong> 280 + {verificationResult.missingPermissions.join(", ")} 281 + </strong> 282 + </p> 283 + <div className="flex gap-2"> 284 + {verificationResult.settingsUrl && ( 285 + <Button 286 + variant="outline" 287 + size="sm" 288 + onClick={() => 289 + window.open(verificationResult.settingsUrl, "_blank") 290 + } 291 + className="gap-2" 292 + > 293 + <ExternalLink className="w-3 h-3" /> 294 + Update Permissions 295 + </Button> 296 + )} 297 + {verificationResult.installationUrl && ( 298 + <Button 299 + variant="outline" 300 + size="sm" 301 + onClick={() => 302 + window.open( 303 + verificationResult.installationUrl, 304 + "_blank", 305 + ) 306 + } 307 + className="gap-2" 308 + > 309 + <ExternalLink className="w-3 h-3" /> 310 + Reinstall App 311 + </Button> 312 + )} 313 + </div> 314 + </div> 315 + )} 316 + 317 + {!verificationResult.isInstalled && 318 + verificationResult.repositoryExists && ( 319 + <div className="mt-2 flex gap-2"> 320 + {verificationResult.installationUrl && ( 321 + <Button 322 + variant="outline" 323 + size="sm" 324 + onClick={() => 325 + window.open( 326 + verificationResult.installationUrl, 327 + "_blank", 328 + ) 329 + } 330 + className="gap-2" 331 + > 332 + <ExternalLink className="w-3 h-3" /> 333 + Install GitHub App 334 + </Button> 335 + )} 336 + {verificationResult.settingsUrl && ( 337 + <Button 338 + variant="outline" 339 + size="sm" 340 + onClick={() => 341 + window.open(verificationResult.settingsUrl, "_blank") 342 + } 343 + className="gap-2" 344 + > 345 + <ExternalLink className="w-3 h-3" /> 346 + View App Details 347 + </Button> 348 + )} 349 + </div> 350 + )} 351 + </div> 352 + </div> 353 + )} 354 + 355 + <Form {...form}> 356 + <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> 357 + <div className="grid grid-cols-2 gap-4"> 358 + <FormField 359 + control={form.control} 360 + name="repositoryOwner" 361 + render={({ field }) => ( 362 + <FormItem> 363 + <FormLabel>Repository Owner</FormLabel> 364 + <FormControl> 365 + <Input 366 + placeholder="e.g., octocat" 367 + {...field} 368 + disabled={isCreating || isDeleting} 369 + /> 370 + </FormControl> 371 + <FormDescription> 372 + The GitHub username or organization name 373 + </FormDescription> 374 + <FormMessage /> 375 + </FormItem> 376 + )} 377 + /> 378 + 379 + <FormField 380 + control={form.control} 381 + name="repositoryName" 382 + render={({ field }) => ( 383 + <FormItem> 384 + <FormLabel>Repository Name</FormLabel> 385 + <FormControl> 386 + <Input 387 + placeholder="e.g., my-project" 388 + {...field} 389 + disabled={isCreating || isDeleting} 390 + /> 391 + </FormControl> 392 + <FormDescription>The repository name</FormDescription> 393 + <FormMessage /> 394 + </FormItem> 395 + )} 396 + /> 397 + </div> 398 + 399 + <div className="flex items-center gap-2"> 400 + <Button 401 + type="button" 402 + variant="outline" 403 + onClick={() => setShowRepositoryBrowser(true)} 404 + className="flex items-center gap-2" 405 + > 406 + <GitBranch className="w-4 h-4" /> 407 + Browse Repositories 408 + </Button> 409 + 410 + <Button 411 + type="button" 412 + variant="outline" 413 + onClick={() => handleVerifyInstallation(form.getValues())} 414 + disabled={isVerifying || !form.formState.isValid} 415 + className="flex items-center gap-2" 416 + > 417 + <RefreshCw 418 + className={cn("w-4 h-4", isVerifying && "animate-spin")} 419 + /> 420 + Verify Installation 421 + </Button> 422 + 423 + <Button 424 + type="submit" 425 + disabled={ 426 + isCreating || 427 + isDeleting || 428 + !form.formState.isValid || 429 + (verificationResult 430 + ? !verificationResult.isInstalled || 431 + !verificationResult.hasRequiredPermissions 432 + : false) 433 + } 434 + className="flex items-center gap-2" 435 + > 436 + <Link className="w-4 h-4" /> 437 + {isConnected ? "Update Integration" : "Connect Repository"} 438 + </Button> 439 + 440 + {isConnected && ( 441 + <Button 442 + type="button" 443 + variant="destructive" 444 + onClick={handleDelete} 445 + disabled={isCreating || isDeleting} 446 + className="flex items-center gap-2" 447 + > 448 + <Unlink className="w-4 h-4" /> 449 + Disconnect 450 + </Button> 451 + )} 452 + </div> 453 + </form> 454 + </Form> 455 + 456 + <RepositoryBrowserModal 457 + open={showRepositoryBrowser} 458 + onOpenChange={setShowRepositoryBrowser} 459 + onSelectRepository={handleRepositorySelect} 460 + selectedRepository={ 461 + repositoryOwner && repositoryName 462 + ? `${repositoryOwner}/${repositoryName}` 463 + : undefined 464 + } 465 + /> 466 + 467 + {!verificationResult?.isInstalled && 468 + repositoryOwner && 469 + repositoryName && ( 470 + <div className="p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg"> 471 + <div className="flex items-start gap-3"> 472 + <Github className="w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5" /> 473 + <div className="text-sm text-blue-700 dark:text-blue-300"> 474 + <p className="font-medium mb-2"> 475 + Need to install the GitHub App? 476 + </p> 477 + <p className="mb-3"> 478 + To use this integration, you need to install the Kaneo GitHub 479 + App on your repository. This gives Kaneo permission to create 480 + issues when tasks are created. 481 + </p> 482 + <div className="space-y-2"> 483 + <p className="text-xs">Steps to install:</p> 484 + <ol className="list-decimal list-inside space-y-1 text-xs"> 485 + <li>Click "Install GitHub App" if available above</li> 486 + <li>Or go to your GitHub App settings</li> 487 + <li> 488 + Select the repository:{" "} 489 + <code className="px-1 py-0.5 bg-blue-100 dark:bg-blue-800 rounded"> 490 + {repositoryOwner}/{repositoryName} 491 + </code> 492 + </li> 493 + <li>Grant necessary permissions (Issues: Write)</li> 494 + <li>Come back and click "Verify Installation"</li> 495 + </ol> 496 + </div> 497 + </div> 498 + </div> 499 + </div> 500 + )} 501 + 502 + {verificationResult?.isInstalled && 503 + !verificationResult.hasRequiredPermissions && ( 504 + <div className="p-4 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg"> 505 + <div className="flex items-start gap-3"> 506 + <AlertTriangle className="w-5 h-5 text-amber-600 dark:text-amber-400 mt-0.5" /> 507 + <div className="text-sm text-amber-700 dark:text-amber-300"> 508 + <p className="font-medium mb-2"> 509 + App installed but permissions need updating 510 + </p> 511 + <p className="mb-3"> 512 + The GitHub App is installed on your repository but doesn't 513 + have the required permissions to create issues. You need to 514 + update the app permissions. 515 + </p> 516 + <div className="space-y-2"> 517 + <p className="text-xs">Required permissions:</p> 518 + <ul className="list-disc list-inside space-y-1 text-xs"> 519 + <li> 520 + Issues: <strong>Write</strong> (to create and update 521 + issues) 522 + </li> 523 + <li> 524 + Metadata: <strong>Read</strong> (to access repository 525 + information) 526 + </li> 527 + </ul> 528 + <p className="text-xs mt-2"> 529 + Click "Update Permissions" above or visit your 530 + <a 531 + href="https://github.com/settings/installations" 532 + target="_blank" 533 + rel="noopener noreferrer" 534 + className="underline hover:no-underline ml-1" 535 + > 536 + GitHub App settings 537 + </a> 538 + to modify permissions. 539 + </p> 540 + </div> 541 + </div> 542 + </div> 543 + </div> 544 + )} 545 + </div> 546 + ); 547 + }
+327
apps/web/src/components/project/repository-browser-modal.tsx
··· 1 + import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; 2 + import { Badge } from "@/components/ui/badge"; 3 + import { Button } from "@/components/ui/button"; 4 + import { Input } from "@/components/ui/input"; 5 + import getGitHubAppInfo from "@/fetchers/github-integration/get-app-info"; 6 + import listRepositories, { 7 + type ListRepositoriesResponse, 8 + } from "@/fetchers/github-integration/list-repositories"; 9 + import { cn } from "@/lib/cn"; 10 + import * as Dialog from "@radix-ui/react-dialog"; 11 + import { useQuery } from "@tanstack/react-query"; 12 + import { 13 + Check, 14 + Clock, 15 + ExternalLink, 16 + GitBranch, 17 + Globe, 18 + Lock, 19 + Search, 20 + Settings, 21 + X, 22 + } from "lucide-react"; 23 + import React from "react"; 24 + 25 + type RepositoryBrowserModalProps = { 26 + open: boolean; 27 + onOpenChange: (open: boolean) => void; 28 + onSelectRepository: (repository: { owner: string; name: string }) => void; 29 + selectedRepository?: string; 30 + }; 31 + 32 + export function RepositoryBrowserModal({ 33 + open, 34 + onOpenChange, 35 + onSelectRepository, 36 + selectedRepository, 37 + }: RepositoryBrowserModalProps) { 38 + const [searchTerm, setSearchTerm] = React.useState(""); 39 + 40 + const { data, isLoading, error, refetch } = useQuery({ 41 + queryKey: ["github-repositories"], 42 + queryFn: listRepositories, 43 + enabled: open, 44 + }); 45 + 46 + const { data: appInfo } = useQuery({ 47 + queryKey: ["github-app-info"], 48 + queryFn: getGitHubAppInfo, 49 + enabled: open, 50 + }); 51 + 52 + const filteredRepositories = React.useMemo(() => { 53 + if (!data?.repositories) return []; 54 + 55 + if (!searchTerm) return data.repositories; 56 + 57 + const search = searchTerm.toLowerCase(); 58 + return data.repositories.filter( 59 + (repo) => 60 + repo.full_name.toLowerCase().includes(search) || 61 + repo.description?.toLowerCase().includes(search), 62 + ); 63 + }, [data?.repositories, searchTerm]); 64 + 65 + const handleSelectRepository = ( 66 + repository: ListRepositoriesResponse["repositories"][number], 67 + ) => { 68 + onSelectRepository({ 69 + owner: repository.owner.login, 70 + name: repository.name, 71 + }); 72 + onOpenChange(false); 73 + }; 74 + 75 + const formatTimeAgo = (dateString: string) => { 76 + const date = new Date(dateString); 77 + const now = new Date(); 78 + const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000); 79 + 80 + if (diffInSeconds < 60) return "just now"; 81 + if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)}m ago`; 82 + if (diffInSeconds < 86400) 83 + return `${Math.floor(diffInSeconds / 3600)}h ago`; 84 + if (diffInSeconds < 2592000) 85 + return `${Math.floor(diffInSeconds / 86400)}d ago`; 86 + 87 + return date.toLocaleDateString(); 88 + }; 89 + 90 + const resetAndCloseModal = (open: boolean) => { 91 + if (!open) { 92 + setSearchTerm(""); 93 + } 94 + onOpenChange(open); 95 + }; 96 + 97 + return ( 98 + <Dialog.Root open={open} onOpenChange={resetAndCloseModal}> 99 + <Dialog.Portal> 100 + <Dialog.Overlay className="fixed inset-0 bg-black/40 backdrop-blur-sm z-40" /> 101 + <Dialog.Content className="fixed z-50 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-3xl max-h-[85vh]"> 102 + <div className="bg-white dark:bg-zinc-900 rounded-lg shadow-xl border border-zinc-200 dark:border-zinc-800 flex flex-col h-full max-h-[85vh]"> 103 + <div className="flex-shrink-0 flex items-center justify-between p-4 border-b border-zinc-200 dark:border-zinc-800"> 104 + <div> 105 + <Dialog.Title className="text-lg font-semibold text-zinc-900 dark:text-zinc-100 flex items-center gap-2"> 106 + <GitBranch className="w-5 h-5" /> 107 + Select Repository 108 + </Dialog.Title> 109 + <p className="text-sm text-zinc-500 dark:text-zinc-400 mt-1"> 110 + Choose a repository where your GitHub App is installed to 111 + enable issue creation. 112 + </p> 113 + </div> 114 + <Dialog.Close className="text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-300"> 115 + <X size={20} /> 116 + </Dialog.Close> 117 + </div> 118 + 119 + <div className="flex-shrink-0 p-4 border-b border-zinc-200 dark:border-zinc-800"> 120 + <div className="relative"> 121 + <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-zinc-400 w-4 h-4" /> 122 + <Input 123 + placeholder="Search repositories..." 124 + value={searchTerm} 125 + onChange={(e) => setSearchTerm(e.target.value)} 126 + className="pl-10 bg-white dark:bg-zinc-800/50" 127 + /> 128 + </div> 129 + </div> 130 + 131 + <div className="flex-1 overflow-y-auto"> 132 + {isLoading && ( 133 + <div className="p-4 space-y-3"> 134 + {Array.from({ length: 5 }, (_, i) => ( 135 + <div 136 + key={`loading-skeleton-repo-${i}-${Date.now()}`} 137 + className="p-3 border border-zinc-200 dark:border-zinc-700 rounded-lg animate-pulse" 138 + > 139 + <div className="flex items-center gap-3"> 140 + <div className="w-8 h-8 bg-zinc-200 dark:bg-zinc-700 rounded-full" /> 141 + <div className="flex-1"> 142 + <div className="w-48 h-4 bg-zinc-200 dark:bg-zinc-700 rounded mb-2" /> 143 + <div className="w-32 h-3 bg-zinc-200 dark:bg-zinc-700 rounded" /> 144 + </div> 145 + </div> 146 + </div> 147 + ))} 148 + </div> 149 + )} 150 + 151 + {error && ( 152 + <div className="text-center py-12"> 153 + <div className="text-red-600 dark:text-red-400 mb-3 font-medium"> 154 + Failed to load repositories 155 + </div> 156 + <Button variant="outline" onClick={() => refetch()}> 157 + Try Again 158 + </Button> 159 + </div> 160 + )} 161 + 162 + {data && !isLoading && ( 163 + <> 164 + {data.repositories.length === 0 && ( 165 + <div className="text-center py-12"> 166 + <GitBranch className="w-12 h-12 text-zinc-400 mx-auto mb-4" /> 167 + <h3 className="text-lg font-medium text-zinc-900 dark:text-zinc-100 mb-2"> 168 + No repositories found 169 + </h3> 170 + <p className="text-zinc-500 dark:text-zinc-400 mb-6 max-w-md mx-auto"> 171 + Install the GitHub App on your repositories to see them 172 + here. 173 + </p> 174 + {appInfo?.appName && ( 175 + <Button 176 + onClick={() => 177 + window.open( 178 + `https://github.com/apps/${appInfo.appName}`, 179 + "_blank", 180 + ) 181 + } 182 + className="bg-indigo-600 text-white hover:bg-indigo-500 dark:bg-indigo-500 dark:hover:bg-indigo-400" 183 + > 184 + <ExternalLink className="w-4 h-4 mr-2" /> 185 + Install GitHub App 186 + </Button> 187 + )} 188 + </div> 189 + )} 190 + 191 + {/* Repository list */} 192 + {filteredRepositories.length > 0 && ( 193 + <div className="p-4 space-y-2"> 194 + {filteredRepositories.map((repository) => ( 195 + <button 196 + key={repository.id} 197 + type="button" 198 + onClick={() => handleSelectRepository(repository)} 199 + className={cn( 200 + "w-full p-3 border rounded-lg text-left transition-all duration-200 group", 201 + "hover:bg-zinc-50 dark:hover:bg-zinc-800/50", 202 + "focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-zinc-900", 203 + selectedRepository === repository.full_name 204 + ? "border-indigo-500 bg-indigo-50 dark:bg-indigo-500/10 dark:border-indigo-400" 205 + : "border-zinc-200 dark:border-zinc-700", 206 + )} 207 + > 208 + <div className="flex items-center justify-between"> 209 + <div className="flex items-center gap-3 flex-1 min-w-0"> 210 + <Avatar className="w-8 h-8 flex-shrink-0"> 211 + <AvatarImage 212 + src={repository.owner.avatar_url} 213 + /> 214 + <AvatarFallback className="bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-400"> 215 + {repository.owner.login 216 + .charAt(0) 217 + .toUpperCase()} 218 + </AvatarFallback> 219 + </Avatar> 220 + 221 + <div className="flex-1 min-w-0"> 222 + <div className="flex items-center gap-2 mb-1"> 223 + <span className="font-medium text-zinc-900 dark:text-zinc-100 truncate"> 224 + {repository.full_name} 225 + </span> 226 + <div className="flex items-center gap-1 flex-shrink-0"> 227 + {repository.private ? ( 228 + <Lock className="w-3 h-3 text-zinc-400" /> 229 + ) : ( 230 + <Globe className="w-3 h-3 text-zinc-400" /> 231 + )} 232 + <Badge 233 + variant="secondary" 234 + className="text-xs bg-zinc-100 dark:bg-zinc-800" 235 + > 236 + {repository.owner.type} 237 + </Badge> 238 + </div> 239 + </div> 240 + 241 + {repository.description && ( 242 + <p className="text-sm text-zinc-600 dark:text-zinc-400 truncate mb-1"> 243 + {repository.description} 244 + </p> 245 + )} 246 + 247 + <div className="flex items-center gap-4 text-xs text-zinc-500 dark:text-zinc-500"> 248 + <div className="flex items-center gap-1"> 249 + <Clock className="w-3 h-3" /> 250 + Updated{" "} 251 + {formatTimeAgo(repository.updated_at)} 252 + </div> 253 + </div> 254 + </div> 255 + </div> 256 + 257 + <div className="flex items-center gap-2 flex-shrink-0 ml-3"> 258 + {selectedRepository === repository.full_name && ( 259 + <Check className="w-4 h-4 text-indigo-600 dark:text-indigo-400" /> 260 + )} 261 + <Button 262 + variant="ghost" 263 + size="sm" 264 + onClick={(e) => { 265 + e.stopPropagation(); 266 + window.open(repository.html_url, "_blank"); 267 + }} 268 + className="opacity-0 group-hover:opacity-100 transition-opacity" 269 + > 270 + <ExternalLink className="w-4 h-4" /> 271 + </Button> 272 + </div> 273 + </div> 274 + </button> 275 + ))} 276 + </div> 277 + )} 278 + 279 + {/* No filtered results */} 280 + {filteredRepositories.length === 0 && 281 + data.repositories.length > 0 && ( 282 + <div className="text-center py-12"> 283 + <Search className="w-12 h-12 text-zinc-400 mx-auto mb-4" /> 284 + <h3 className="text-lg font-medium text-zinc-900 dark:text-zinc-100 mb-2"> 285 + No repositories match your search 286 + </h3> 287 + <p className="text-zinc-500 dark:text-zinc-400"> 288 + Try adjusting your search terms or clear the search to 289 + see all repositories. 290 + </p> 291 + </div> 292 + )} 293 + </> 294 + )} 295 + </div> 296 + 297 + {/* Footer */} 298 + {data && data.installations.length > 0 && ( 299 + <div className="flex-shrink-0 border-t border-zinc-200 dark:border-zinc-800 p-4"> 300 + <div className="flex items-center justify-between text-sm text-zinc-600 dark:text-zinc-400"> 301 + <span> 302 + {data.repositories.length} repositories across{" "} 303 + {data.installations.length} installations 304 + </span> 305 + <Button 306 + variant="ghost" 307 + size="sm" 308 + onClick={() => 309 + window.open( 310 + "https://github.com/settings/installations", 311 + "_blank", 312 + ) 313 + } 314 + className="text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-100" 315 + > 316 + <Settings className="w-4 h-4 mr-2" /> 317 + Manage Installations 318 + </Button> 319 + </div> 320 + </div> 321 + )} 322 + </div> 323 + </Dialog.Content> 324 + </Dialog.Portal> 325 + </Dialog.Root> 326 + ); 327 + }
+28
apps/web/src/fetchers/github-integration/create-github-integration.ts
··· 1 + import { client } from "@kaneo/libs"; 2 + import type { InferRequestType } from "hono"; 3 + 4 + export type CreateGithubIntegrationRequest = InferRequestType< 5 + (typeof client)["github-integration"]["project"][":projectId"]["$post"] 6 + >["json"]; 7 + 8 + async function createGithubIntegration( 9 + projectId: string, 10 + data: CreateGithubIntegrationRequest, 11 + ) { 12 + const response = await client["github-integration"].project[ 13 + ":projectId" 14 + ].$post({ 15 + param: { projectId }, 16 + json: data, 17 + }); 18 + 19 + if (!response.ok) { 20 + const error = await response.text(); 21 + throw new Error(error); 22 + } 23 + 24 + const result = await response.json(); 25 + return result; 26 + } 27 + 28 + export default createGithubIntegration;
+19
apps/web/src/fetchers/github-integration/delete-github-integration.ts
··· 1 + import { client } from "@kaneo/libs"; 2 + 3 + async function deleteGithubIntegration(projectId: string) { 4 + const response = await client["github-integration"].project[ 5 + ":projectId" 6 + ].$delete({ 7 + param: { projectId }, 8 + }); 9 + 10 + if (!response.ok) { 11 + const error = await response.text(); 12 + throw new Error(error); 13 + } 14 + 15 + const result = await response.json(); 16 + return result; 17 + } 18 + 19 + export default deleteGithubIntegration;
+18
apps/web/src/fetchers/github-integration/get-app-info.ts
··· 1 + import { client } from "@kaneo/libs"; 2 + import type { InferResponseType } from "hono"; 3 + 4 + export type GitHubAppInfo = InferResponseType< 5 + (typeof client)["github-integration"]["app-info"]["$get"] 6 + >; 7 + 8 + export default async function getGitHubAppInfo(): Promise<GitHubAppInfo> { 9 + const response = await client["github-integration"]["app-info"].$get(); 10 + 11 + if (!response.ok) { 12 + const error = await response.text(); 13 + throw new Error(error); 14 + } 15 + 16 + const result = await response.json(); 17 + return result; 18 + }
+19
apps/web/src/fetchers/github-integration/get-github-integration.ts
··· 1 + import { client } from "@kaneo/libs"; 2 + 3 + async function getGithubIntegration(projectId: string) { 4 + const response = await client["github-integration"].project[ 5 + ":projectId" 6 + ].$get({ 7 + param: { projectId }, 8 + }); 9 + 10 + if (!response.ok) { 11 + const error = await response.text(); 12 + throw new Error(error); 13 + } 14 + 15 + const data = await response.json(); 16 + return data; 17 + } 18 + 19 + export default getGithubIntegration;
+20
apps/web/src/fetchers/github-integration/list-repositories.ts
··· 1 + import { client } from "@kaneo/libs"; 2 + import type { InferResponseType } from "hono"; 3 + 4 + export type ListRepositoriesResponse = InferResponseType< 5 + (typeof client)["github-integration"]["repositories"]["$get"] 6 + >; 7 + 8 + async function listRepositories(): Promise<ListRepositoriesResponse> { 9 + const response = await client["github-integration"].repositories.$get(); 10 + 11 + if (!response.ok) { 12 + const error = await response.text(); 13 + throw new Error(error); 14 + } 15 + 16 + const result = await response.json(); 17 + return result; 18 + } 19 + 20 + export default listRepositories;
+28
apps/web/src/fetchers/github-integration/verify-github-installation.ts
··· 1 + import { client } from "@kaneo/libs"; 2 + import type { InferRequestType, InferResponseType } from "hono"; 3 + 4 + export type VerifyGithubInstallationRequest = InferRequestType< 5 + (typeof client)["github-integration"]["verify"]["$post"] 6 + >["json"]; 7 + 8 + export type VerifyGithubInstallationResponse = InferResponseType< 9 + (typeof client)["github-integration"]["verify"]["$post"] 10 + >; 11 + 12 + async function verifyGithubInstallation( 13 + data: VerifyGithubInstallationRequest, 14 + ): Promise<VerifyGithubInstallationResponse> { 15 + const response = await client["github-integration"].verify.$post({ 16 + json: data, 17 + }); 18 + 19 + if (!response.ok) { 20 + const error = await response.text(); 21 + throw new Error(error); 22 + } 23 + 24 + const result = await response.json(); 25 + return result; 26 + } 27 + 28 + export default verifyGithubInstallation;
-1
apps/web/src/fetchers/user/sign-in.ts
··· 11 11 12 12 if (!response.ok) { 13 13 const error = await response.text(); 14 - console.log(error); 15 14 throw new Error(error); 16 15 } 17 16
+47
apps/web/src/hooks/mutations/github-integration/use-create-github-integration.ts
··· 1 + import createGithubIntegration, { 2 + type CreateGithubIntegrationRequest, 3 + } from "@/fetchers/github-integration/create-github-integration"; 4 + import deleteGithubIntegration from "@/fetchers/github-integration/delete-github-integration"; 5 + import verifyGithubInstallation, { 6 + type VerifyGithubInstallationRequest, 7 + } from "@/fetchers/github-integration/verify-github-installation"; 8 + import { useMutation, useQueryClient } from "@tanstack/react-query"; 9 + 10 + export function useCreateGithubIntegration() { 11 + const queryClient = useQueryClient(); 12 + 13 + return useMutation({ 14 + mutationFn: ({ 15 + projectId, 16 + data, 17 + }: { 18 + projectId: string; 19 + data: CreateGithubIntegrationRequest; 20 + }) => createGithubIntegration(projectId, data), 21 + onSuccess: (_, { projectId }) => { 22 + queryClient.invalidateQueries({ 23 + queryKey: ["github-integration", projectId], 24 + }); 25 + }, 26 + }); 27 + } 28 + 29 + export function useDeleteGithubIntegration() { 30 + const queryClient = useQueryClient(); 31 + 32 + return useMutation({ 33 + mutationFn: (projectId: string) => deleteGithubIntegration(projectId), 34 + onSuccess: (_, projectId) => { 35 + queryClient.invalidateQueries({ 36 + queryKey: ["github-integration", projectId], 37 + }); 38 + }, 39 + }); 40 + } 41 + 42 + export function useVerifyGithubInstallation() { 43 + return useMutation({ 44 + mutationFn: (data: VerifyGithubInstallationRequest) => 45 + verifyGithubInstallation(data), 46 + }); 47 + }
+12
apps/web/src/hooks/queries/github-integration/use-get-github-integration.ts
··· 1 + import getGithubIntegration from "@/fetchers/github-integration/get-github-integration"; 2 + import { useQuery } from "@tanstack/react-query"; 3 + 4 + function useGetGithubIntegration(projectId: string) { 5 + return useQuery({ 6 + queryKey: ["github-integration", projectId], 7 + queryFn: () => getGithubIntegration(projectId), 8 + enabled: !!projectId, 9 + }); 10 + } 11 + 12 + export default useGetGithubIntegration;
+1
apps/web/src/query-client/index.ts
··· 5 5 queries: { 6 6 refetchOnWindowFocus: false, 7 7 refetchOnMount: false, 8 + retry: false, 8 9 }, 9 10 }, 10 11 });
+16
apps/web/src/routes/dashboard/workspace/$workspaceId/project/$projectId/settings.tsx
··· 1 1 import PageTitle from "@/components/page-title"; 2 + import { GitHubIntegrationSettings } from "@/components/project/github-integration-settings"; 2 3 import { TasksImportExport } from "@/components/project/tasks-import-export"; 3 4 import { Button } from "@/components/ui/button"; 4 5 import { ··· 444 445 </p> 445 446 446 447 <TasksImportExport project={project} /> 448 + </div> 449 + </div> 450 + )} 451 + 452 + {project && ( 453 + <div className="bg-white dark:bg-zinc-900 rounded-lg border border-zinc-200 dark:border-zinc-800"> 454 + <div className="p-4 md:p-6"> 455 + <h2 className="text-sm font-medium text-zinc-900 dark:text-zinc-100 mb-1"> 456 + GitHub Integration 457 + </h2> 458 + <p className="text-sm text-zinc-500 dark:text-zinc-400 mb-4"> 459 + Configure GitHub integration for your project. 460 + </p> 461 + 462 + <GitHubIntegrationSettings projectId={project.id} /> 447 463 </div> 448 464 </div> 449 465 )}
+356 -3
pnpm-lock.yaml
··· 38 38 '@hono/zod-validator': 39 39 specifier: ^0.5.0 40 40 version: 0.5.0(hono@4.8.0)(zod@3.25.67) 41 + '@octokit/webhooks': 42 + specifier: ^14.0.2 43 + version: 14.0.2 41 44 '@oslojs/crypto': 42 45 specifier: ^1.0.1 43 46 version: 1.0.1 ··· 62 65 hono: 63 66 specifier: ^4.7.11 64 67 version: 4.8.0 68 + octokit: 69 + specifier: ^5.0.3 70 + version: 5.0.3 65 71 pg: 66 72 specifier: ^8.16.0 67 73 version: 8.16.1 ··· 289 295 tailwindcss-animate: 290 296 specifier: ^1.0.7 291 297 version: 1.0.7(tailwindcss@4.1.10) 298 + tiptap-markdown: 299 + specifier: ^0.8.10 300 + version: 0.8.10(@tiptap/core@2.14.0(@tiptap/pm@2.14.0)) 292 301 zod: 293 302 specifier: ^3.25.67 294 303 version: 3.25.67 ··· 1219 1228 resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} 1220 1229 engines: {node: '>= 8'} 1221 1230 1231 + '@octokit/app@16.0.1': 1232 + resolution: {integrity: sha512-kgTeTsWmpUX+s3Fs4EK4w1K+jWCDB6ClxLSWUWTyhlw7+L3jHtuXDR4QtABu2GsmCMdk67xRhruiXotS3ay3Yw==} 1233 + engines: {node: '>= 20'} 1234 + 1235 + '@octokit/auth-app@8.0.1': 1236 + resolution: {integrity: sha512-P2J5pB3pjiGwtJX4WqJVYCtNkcZ+j5T2Wm14aJAEIC3WJOrv12jvBley3G1U/XI8q9o1A7QMG54LiFED2BiFlg==} 1237 + engines: {node: '>= 20'} 1238 + 1239 + '@octokit/auth-oauth-app@9.0.1': 1240 + resolution: {integrity: sha512-TthWzYxuHKLAbmxdFZwFlmwVyvynpyPmjwc+2/cI3cvbT7mHtsAW9b1LvQaNnAuWL+pFnqtxdmrU8QpF633i1g==} 1241 + engines: {node: '>= 20'} 1242 + 1243 + '@octokit/auth-oauth-device@8.0.1': 1244 + resolution: {integrity: sha512-TOqId/+am5yk9zor0RGibmlqn4V0h8vzjxlw/wYr3qzkQxl8aBPur384D1EyHtqvfz0syeXji4OUvKkHvxk/Gw==} 1245 + engines: {node: '>= 20'} 1246 + 1247 + '@octokit/auth-oauth-user@6.0.0': 1248 + resolution: {integrity: sha512-GV9IW134PHsLhtUad21WIeP9mlJ+QNpFd6V9vuPWmaiN25HEJeEQUcS4y5oRuqCm9iWDLtfIs+9K8uczBXKr6A==} 1249 + engines: {node: '>= 20'} 1250 + 1251 + '@octokit/auth-token@6.0.0': 1252 + resolution: {integrity: sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==} 1253 + engines: {node: '>= 20'} 1254 + 1255 + '@octokit/auth-unauthenticated@7.0.1': 1256 + resolution: {integrity: sha512-qVq1vdjLLZdE8kH2vDycNNjuJRCD1q2oet1nA/GXWaYlpDxlR7rdVhX/K/oszXslXiQIiqrQf+rdhDlA99JdTQ==} 1257 + engines: {node: '>= 20'} 1258 + 1259 + '@octokit/core@7.0.2': 1260 + resolution: {integrity: sha512-ODsoD39Lq6vR6aBgvjTnA3nZGliknKboc9Gtxr7E4WDNqY24MxANKcuDQSF0jzapvGb3KWOEDrKfve4HoWGK+g==} 1261 + engines: {node: '>= 20'} 1262 + 1263 + '@octokit/endpoint@11.0.0': 1264 + resolution: {integrity: sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==} 1265 + engines: {node: '>= 20'} 1266 + 1267 + '@octokit/graphql@9.0.1': 1268 + resolution: {integrity: sha512-j1nQNU1ZxNFx2ZtKmL4sMrs4egy5h65OMDmSbVyuCzjOcwsHq6EaYjOTGXPQxgfiN8dJ4CriYHk6zF050WEULg==} 1269 + engines: {node: '>= 20'} 1270 + 1271 + '@octokit/oauth-app@8.0.1': 1272 + resolution: {integrity: sha512-QnhMYEQpnYbEPn9cae+wXL2LuPMFglmfeuDJXXsyxIXdoORwkLK8y0cHhd/5du9MbO/zdG/BXixzB7EEwU63eQ==} 1273 + engines: {node: '>= 20'} 1274 + 1275 + '@octokit/oauth-authorization-url@8.0.0': 1276 + resolution: {integrity: sha512-7QoLPRh/ssEA/HuHBHdVdSgF8xNLz/Bc5m9fZkArJE5bb6NmVkDm3anKxXPmN1zh6b5WKZPRr3697xKT/yM3qQ==} 1277 + engines: {node: '>= 20'} 1278 + 1279 + '@octokit/oauth-methods@6.0.0': 1280 + resolution: {integrity: sha512-Q8nFIagNLIZgM2odAraelMcDssapc+lF+y3OlcIPxyAU+knefO8KmozGqfnma1xegRDP4z5M73ABsamn72bOcA==} 1281 + engines: {node: '>= 20'} 1282 + 1283 + '@octokit/openapi-types@25.1.0': 1284 + resolution: {integrity: sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==} 1285 + 1286 + '@octokit/openapi-webhooks-types@12.0.3': 1287 + resolution: {integrity: sha512-90MF5LVHjBedwoHyJsgmaFhEN1uzXyBDRLEBe7jlTYx/fEhPAk3P3DAJsfZwC54m8hAIryosJOL+UuZHB3K3yA==} 1288 + 1289 + '@octokit/plugin-paginate-graphql@6.0.0': 1290 + resolution: {integrity: sha512-crfpnIoFiBtRkvPqOyLOsw12XsveYuY2ieP6uYDosoUegBJpSVxGwut9sxUgFFcll3VTOTqpUf8yGd8x1OmAkQ==} 1291 + engines: {node: '>= 20'} 1292 + peerDependencies: 1293 + '@octokit/core': '>=6' 1294 + 1295 + '@octokit/plugin-paginate-rest@13.1.0': 1296 + resolution: {integrity: sha512-16iNOa4rTTjaWtfsPGJcYYL79FJakseX8TQFIPfVuSPC3s5nkS/DSNQPFPc5lJHgEDBWNMxSApHrEymNblhA9w==} 1297 + engines: {node: '>= 20'} 1298 + peerDependencies: 1299 + '@octokit/core': '>=6' 1300 + 1301 + '@octokit/plugin-rest-endpoint-methods@16.0.0': 1302 + resolution: {integrity: sha512-kJVUQk6/dx/gRNLWUnAWKFs1kVPn5O5CYZyssyEoNYaFedqZxsfYs7DwI3d67hGz4qOwaJ1dpm07hOAD1BXx6g==} 1303 + engines: {node: '>= 20'} 1304 + peerDependencies: 1305 + '@octokit/core': '>=6' 1306 + 1307 + '@octokit/plugin-retry@8.0.1': 1308 + resolution: {integrity: sha512-KUoYR77BjF5O3zcwDQHRRZsUvJwepobeqiSSdCJ8lWt27FZExzb0GgVxrhhfuyF6z2B2zpO0hN5pteni1sqWiw==} 1309 + engines: {node: '>= 20'} 1310 + peerDependencies: 1311 + '@octokit/core': '>=7' 1312 + 1313 + '@octokit/plugin-throttling@11.0.1': 1314 + resolution: {integrity: sha512-S+EVhy52D/272L7up58dr3FNSMXWuNZolkL4zMJBNIfIxyZuUcczsQAU4b5w6dewJXnKYVgSHSV5wxitMSW1kw==} 1315 + engines: {node: '>= 20'} 1316 + peerDependencies: 1317 + '@octokit/core': ^7.0.0 1318 + 1319 + '@octokit/request-error@7.0.0': 1320 + resolution: {integrity: sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==} 1321 + engines: {node: '>= 20'} 1322 + 1323 + '@octokit/request@10.0.2': 1324 + resolution: {integrity: sha512-iYj4SJG/2bbhh+iIpFmG5u49DtJ4lipQ+aPakjL9OKpsGY93wM8w06gvFbEQxcMsZcCvk5th5KkIm2m8o14aWA==} 1325 + engines: {node: '>= 20'} 1326 + 1327 + '@octokit/types@14.1.0': 1328 + resolution: {integrity: sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==} 1329 + 1330 + '@octokit/webhooks-methods@6.0.0': 1331 + resolution: {integrity: sha512-MFlzzoDJVw/GcbfzVC1RLR36QqkTLUf79vLVO3D+xn7r0QgxnFoLZgtrzxiQErAjFUOdH6fas2KeQJ1yr/qaXQ==} 1332 + engines: {node: '>= 20'} 1333 + 1334 + '@octokit/webhooks@14.0.2': 1335 + resolution: {integrity: sha512-16TtZXNOfH8RaRsV+iag5dTYeJvdOdZDBcpEPCULdKS3eTRJqAYxBNZPFaDJ3cx3WNyvbaQ0IxsPpnaR/tgGFA==} 1336 + engines: {node: '>= 20'} 1337 + 1222 1338 '@orama/orama@3.1.6': 1223 1339 resolution: {integrity: sha512-qtSrqCqRU93SjEBedz987tvWao1YQSELjBhGkHYGVP7Dg0lBWP6d+uZEIt5gxTAYio/YWWlhivmRABvRfPLmnQ==} 1224 1340 engines: {node: '>= 16.0.0'} ··· 2451 2567 '@tiptap/starter-kit@2.14.0': 2452 2568 resolution: {integrity: sha512-Z1bKAfHl14quRI3McmdU+bs675jp6/iexEQTI9M9oHa6l3McFF38g9N3xRpPPX02MX83DghsUPupndUW/yJvEQ==} 2453 2569 2570 + '@types/aws-lambda@8.10.150': 2571 + resolution: {integrity: sha512-AX+AbjH/rH5ezX1fbK8onC/a+HyQHo7QGmvoxAE42n22OsciAxvZoZNEr22tbXs8WfP1nIsBjKDpgPm3HjOZbA==} 2572 + 2454 2573 '@types/babel__core@7.20.5': 2455 2574 resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} 2456 2575 ··· 2487 2606 '@types/hast@3.0.4': 2488 2607 resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} 2489 2608 2609 + '@types/linkify-it@3.0.5': 2610 + resolution: {integrity: sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==} 2611 + 2490 2612 '@types/linkify-it@5.0.0': 2491 2613 resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} 2492 2614 2615 + '@types/markdown-it@13.0.9': 2616 + resolution: {integrity: sha512-1XPwR0+MgXLWfTn9gCsZ55AHOKW1WN+P9vr0PaQh5aerR9LLQXUbjfEAFhjmEmyoYFWAyuN2Mqkn40MZ4ukjBw==} 2617 + 2493 2618 '@types/markdown-it@14.1.2': 2494 2619 resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} 2495 2620 2496 2621 '@types/mdast@4.0.4': 2497 2622 resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} 2623 + 2624 + '@types/mdurl@1.0.5': 2625 + resolution: {integrity: sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==} 2498 2626 2499 2627 '@types/mdurl@2.0.0': 2500 2628 resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} ··· 2653 2781 resolution: {integrity: sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==} 2654 2782 engines: {node: '>= 18'} 2655 2783 2784 + before-after-hook@4.0.0: 2785 + resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==} 2786 + 2656 2787 better-sqlite3@11.10.0: 2657 2788 resolution: {integrity: sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==} 2658 2789 ··· 2665 2796 2666 2797 bl@4.1.0: 2667 2798 resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} 2799 + 2800 + bottleneck@2.19.5: 2801 + resolution: {integrity: sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==} 2668 2802 2669 2803 braces@3.0.3: 2670 2804 resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} ··· 3091 3225 extend@3.0.2: 3092 3226 resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} 3093 3227 3228 + fast-content-type-parse@3.0.0: 3229 + resolution: {integrity: sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==} 3230 + 3094 3231 fast-deep-equal@3.1.3: 3095 3232 resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} 3096 3233 ··· 3532 3669 resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==} 3533 3670 engines: {node: '>=16'} 3534 3671 3672 + markdown-it-task-lists@2.1.1: 3673 + resolution: {integrity: sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA==} 3674 + 3535 3675 markdown-it@14.1.0: 3536 3676 resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} 3537 3677 hasBin: true ··· 3811 3951 resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} 3812 3952 engines: {node: '>=0.10.0'} 3813 3953 3954 + octokit@5.0.3: 3955 + resolution: {integrity: sha512-+bwYsAIRmYv30NTmBysPIlgH23ekVDriB07oRxlPIAH5PI0yTMSxg5i5Xy0OetcnZw+nk/caD4szD7a9YZ3QyQ==} 3956 + engines: {node: '>= 20'} 3957 + 3814 3958 once@1.4.0: 3815 3959 resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} 3816 3960 ··· 4384 4528 tippy.js@6.3.7: 4385 4529 resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==} 4386 4530 4531 + tiptap-markdown@0.8.10: 4532 + resolution: {integrity: sha512-iDVkR2BjAqkTDtFX0h94yVvE2AihCXlF0Q7RIXSJPRSR5I0PA1TMuAg6FHFpmqTn4tPxJ0by0CK7PUMlnFLGEQ==} 4533 + peerDependencies: 4534 + '@tiptap/core': ^2.0.3 4535 + 4387 4536 to-regex-range@5.0.1: 4388 4537 resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 4389 4538 engines: {node: '>=8.0'} 4539 + 4540 + toad-cache@3.7.0: 4541 + resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} 4542 + engines: {node: '>=12'} 4390 4543 4391 4544 trim-lines@3.0.1: 4392 4545 resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} ··· 4483 4636 unist-util-visit@5.0.0: 4484 4637 resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} 4485 4638 4639 + universal-github-app-jwt@2.2.2: 4640 + resolution: {integrity: sha512-dcmbeSrOdTnsjGjUfAlqNDJrhxXizjAz94ija9Qw8YkZ1uu0d+GoZzyH+Jb9tIIqvGsadUfwg+22k5aDqqwzbw==} 4641 + 4642 + universal-user-agent@7.0.3: 4643 + resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==} 4644 + 4486 4645 unplugin@2.2.2: 4487 4646 resolution: {integrity: sha512-Qp+iiD+qCRnUek+nDoYvtWX7tfnYyXsrOnJ452FRTgOyKmTM7TUJ3l+PLPJOOWPTUyKISKp4isC5JJPSXUjGgw==} 4488 4647 engines: {node: '>=18.12.0'} ··· 5414 5573 '@nodelib/fs.scandir': 2.1.5 5415 5574 fastq: 1.19.1 5416 5575 5576 + '@octokit/app@16.0.1': 5577 + dependencies: 5578 + '@octokit/auth-app': 8.0.1 5579 + '@octokit/auth-unauthenticated': 7.0.1 5580 + '@octokit/core': 7.0.2 5581 + '@octokit/oauth-app': 8.0.1 5582 + '@octokit/plugin-paginate-rest': 13.1.0(@octokit/core@7.0.2) 5583 + '@octokit/types': 14.1.0 5584 + '@octokit/webhooks': 14.0.2 5585 + 5586 + '@octokit/auth-app@8.0.1': 5587 + dependencies: 5588 + '@octokit/auth-oauth-app': 9.0.1 5589 + '@octokit/auth-oauth-user': 6.0.0 5590 + '@octokit/request': 10.0.2 5591 + '@octokit/request-error': 7.0.0 5592 + '@octokit/types': 14.1.0 5593 + toad-cache: 3.7.0 5594 + universal-github-app-jwt: 2.2.2 5595 + universal-user-agent: 7.0.3 5596 + 5597 + '@octokit/auth-oauth-app@9.0.1': 5598 + dependencies: 5599 + '@octokit/auth-oauth-device': 8.0.1 5600 + '@octokit/auth-oauth-user': 6.0.0 5601 + '@octokit/request': 10.0.2 5602 + '@octokit/types': 14.1.0 5603 + universal-user-agent: 7.0.3 5604 + 5605 + '@octokit/auth-oauth-device@8.0.1': 5606 + dependencies: 5607 + '@octokit/oauth-methods': 6.0.0 5608 + '@octokit/request': 10.0.2 5609 + '@octokit/types': 14.1.0 5610 + universal-user-agent: 7.0.3 5611 + 5612 + '@octokit/auth-oauth-user@6.0.0': 5613 + dependencies: 5614 + '@octokit/auth-oauth-device': 8.0.1 5615 + '@octokit/oauth-methods': 6.0.0 5616 + '@octokit/request': 10.0.2 5617 + '@octokit/types': 14.1.0 5618 + universal-user-agent: 7.0.3 5619 + 5620 + '@octokit/auth-token@6.0.0': {} 5621 + 5622 + '@octokit/auth-unauthenticated@7.0.1': 5623 + dependencies: 5624 + '@octokit/request-error': 7.0.0 5625 + '@octokit/types': 14.1.0 5626 + 5627 + '@octokit/core@7.0.2': 5628 + dependencies: 5629 + '@octokit/auth-token': 6.0.0 5630 + '@octokit/graphql': 9.0.1 5631 + '@octokit/request': 10.0.2 5632 + '@octokit/request-error': 7.0.0 5633 + '@octokit/types': 14.1.0 5634 + before-after-hook: 4.0.0 5635 + universal-user-agent: 7.0.3 5636 + 5637 + '@octokit/endpoint@11.0.0': 5638 + dependencies: 5639 + '@octokit/types': 14.1.0 5640 + universal-user-agent: 7.0.3 5641 + 5642 + '@octokit/graphql@9.0.1': 5643 + dependencies: 5644 + '@octokit/request': 10.0.2 5645 + '@octokit/types': 14.1.0 5646 + universal-user-agent: 7.0.3 5647 + 5648 + '@octokit/oauth-app@8.0.1': 5649 + dependencies: 5650 + '@octokit/auth-oauth-app': 9.0.1 5651 + '@octokit/auth-oauth-user': 6.0.0 5652 + '@octokit/auth-unauthenticated': 7.0.1 5653 + '@octokit/core': 7.0.2 5654 + '@octokit/oauth-authorization-url': 8.0.0 5655 + '@octokit/oauth-methods': 6.0.0 5656 + '@types/aws-lambda': 8.10.150 5657 + universal-user-agent: 7.0.3 5658 + 5659 + '@octokit/oauth-authorization-url@8.0.0': {} 5660 + 5661 + '@octokit/oauth-methods@6.0.0': 5662 + dependencies: 5663 + '@octokit/oauth-authorization-url': 8.0.0 5664 + '@octokit/request': 10.0.2 5665 + '@octokit/request-error': 7.0.0 5666 + '@octokit/types': 14.1.0 5667 + 5668 + '@octokit/openapi-types@25.1.0': {} 5669 + 5670 + '@octokit/openapi-webhooks-types@12.0.3': {} 5671 + 5672 + '@octokit/plugin-paginate-graphql@6.0.0(@octokit/core@7.0.2)': 5673 + dependencies: 5674 + '@octokit/core': 7.0.2 5675 + 5676 + '@octokit/plugin-paginate-rest@13.1.0(@octokit/core@7.0.2)': 5677 + dependencies: 5678 + '@octokit/core': 7.0.2 5679 + '@octokit/types': 14.1.0 5680 + 5681 + '@octokit/plugin-rest-endpoint-methods@16.0.0(@octokit/core@7.0.2)': 5682 + dependencies: 5683 + '@octokit/core': 7.0.2 5684 + '@octokit/types': 14.1.0 5685 + 5686 + '@octokit/plugin-retry@8.0.1(@octokit/core@7.0.2)': 5687 + dependencies: 5688 + '@octokit/core': 7.0.2 5689 + '@octokit/request-error': 7.0.0 5690 + '@octokit/types': 14.1.0 5691 + bottleneck: 2.19.5 5692 + 5693 + '@octokit/plugin-throttling@11.0.1(@octokit/core@7.0.2)': 5694 + dependencies: 5695 + '@octokit/core': 7.0.2 5696 + '@octokit/types': 14.1.0 5697 + bottleneck: 2.19.5 5698 + 5699 + '@octokit/request-error@7.0.0': 5700 + dependencies: 5701 + '@octokit/types': 14.1.0 5702 + 5703 + '@octokit/request@10.0.2': 5704 + dependencies: 5705 + '@octokit/endpoint': 11.0.0 5706 + '@octokit/request-error': 7.0.0 5707 + '@octokit/types': 14.1.0 5708 + fast-content-type-parse: 3.0.0 5709 + universal-user-agent: 7.0.3 5710 + 5711 + '@octokit/types@14.1.0': 5712 + dependencies: 5713 + '@octokit/openapi-types': 25.1.0 5714 + 5715 + '@octokit/webhooks-methods@6.0.0': {} 5716 + 5717 + '@octokit/webhooks@14.0.2': 5718 + dependencies: 5719 + '@octokit/openapi-webhooks-types': 12.0.3 5720 + '@octokit/request-error': 7.0.0 5721 + '@octokit/webhooks-methods': 6.0.0 5722 + 5417 5723 '@orama/orama@3.1.6': {} 5418 5724 5419 5725 '@oslojs/asn1@1.0.0': ··· 6671 6977 '@tiptap/extension-text-style': 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0)) 6672 6978 '@tiptap/pm': 2.14.0 6673 6979 6980 + '@types/aws-lambda@8.10.150': {} 6981 + 6674 6982 '@types/babel__core@7.20.5': 6675 6983 dependencies: 6676 6984 '@babel/parser': 7.27.0 ··· 6698 7006 6699 7007 '@types/better-sqlite3@7.6.13': 6700 7008 dependencies: 6701 - '@types/node': 22.15.32 7009 + '@types/node': 24.0.3 6702 7010 optional: true 6703 7011 6704 7012 '@types/conventional-commits-parser@5.0.1': ··· 6721 7029 dependencies: 6722 7030 '@types/unist': 3.0.3 6723 7031 7032 + '@types/linkify-it@3.0.5': {} 7033 + 6724 7034 '@types/linkify-it@5.0.0': {} 6725 7035 7036 + '@types/markdown-it@13.0.9': 7037 + dependencies: 7038 + '@types/linkify-it': 3.0.5 7039 + '@types/mdurl': 1.0.5 7040 + 6726 7041 '@types/markdown-it@14.1.2': 6727 7042 dependencies: 6728 7043 '@types/linkify-it': 5.0.0 ··· 6731 7046 '@types/mdast@4.0.4': 6732 7047 dependencies: 6733 7048 '@types/unist': 3.0.3 7049 + 7050 + '@types/mdurl@1.0.5': {} 6734 7051 6735 7052 '@types/mdurl@2.0.0': {} 6736 7053 ··· 6772 7089 6773 7090 '@types/ws@8.18.1': 6774 7091 dependencies: 6775 - '@types/node': 22.15.32 7092 + '@types/node': 24.0.3 6776 7093 optional: true 6777 7094 6778 7095 '@ungap/structured-clone@1.3.0': {} ··· 6892 7209 node-addon-api: 8.3.1 6893 7210 node-gyp-build: 4.8.4 6894 7211 7212 + before-after-hook@4.0.0: {} 7213 + 6895 7214 better-sqlite3@11.10.0: 6896 7215 dependencies: 6897 7216 bindings: 1.5.0 ··· 6912 7231 readable-stream: 3.6.2 6913 7232 optional: true 6914 7233 7234 + bottleneck@2.19.5: {} 7235 + 6915 7236 braces@3.0.3: 6916 7237 dependencies: 6917 7238 fill-range: 7.1.1 ··· 6933 7254 6934 7255 bun-types@1.2.8: 6935 7256 dependencies: 6936 - '@types/node': 22.15.32 7257 + '@types/node': 24.0.3 6937 7258 '@types/ws': 8.18.1 6938 7259 optional: true 6939 7260 ··· 7299 7620 is-extendable: 0.1.1 7300 7621 7301 7622 extend@3.0.2: {} 7623 + 7624 + fast-content-type-parse@3.0.0: {} 7302 7625 7303 7626 fast-deep-equal@3.1.3: {} 7304 7627 ··· 7733 8056 7734 8057 markdown-extensions@2.0.0: {} 7735 8058 8059 + markdown-it-task-lists@2.1.1: {} 8060 + 7736 8061 markdown-it@14.1.0: 7737 8062 dependencies: 7738 8063 argparse: 2.0.1 ··· 8264 8589 8265 8590 normalize-path@3.0.0: {} 8266 8591 8592 + octokit@5.0.3: 8593 + dependencies: 8594 + '@octokit/app': 16.0.1 8595 + '@octokit/core': 7.0.2 8596 + '@octokit/oauth-app': 8.0.1 8597 + '@octokit/plugin-paginate-graphql': 6.0.0(@octokit/core@7.0.2) 8598 + '@octokit/plugin-paginate-rest': 13.1.0(@octokit/core@7.0.2) 8599 + '@octokit/plugin-rest-endpoint-methods': 16.0.0(@octokit/core@7.0.2) 8600 + '@octokit/plugin-retry': 8.0.1(@octokit/core@7.0.2) 8601 + '@octokit/plugin-throttling': 11.0.1(@octokit/core@7.0.2) 8602 + '@octokit/request-error': 7.0.0 8603 + '@octokit/types': 14.1.0 8604 + '@octokit/webhooks': 14.0.2 8605 + 8267 8606 once@1.4.0: 8268 8607 dependencies: 8269 8608 wrappy: 1.0.2 ··· 8995 9334 dependencies: 8996 9335 '@popperjs/core': 2.11.8 8997 9336 9337 + tiptap-markdown@0.8.10(@tiptap/core@2.14.0(@tiptap/pm@2.14.0)): 9338 + dependencies: 9339 + '@tiptap/core': 2.14.0(@tiptap/pm@2.14.0) 9340 + '@types/markdown-it': 13.0.9 9341 + markdown-it: 14.1.0 9342 + markdown-it-task-lists: 2.1.1 9343 + prosemirror-markdown: 1.13.2 9344 + 8998 9345 to-regex-range@5.0.1: 8999 9346 dependencies: 9000 9347 is-number: 7.0.0 9348 + 9349 + toad-cache@3.7.0: {} 9001 9350 9002 9351 trim-lines@3.0.1: {} 9003 9352 ··· 9092 9441 '@types/unist': 3.0.3 9093 9442 unist-util-is: 6.0.0 9094 9443 unist-util-visit-parents: 6.0.1 9444 + 9445 + universal-github-app-jwt@2.2.2: {} 9446 + 9447 + universal-user-agent@7.0.3: {} 9095 9448 9096 9449 unplugin@2.2.2: 9097 9450 dependencies: