diff --git a/.changeset/new-facts-pay.md b/.changeset/new-facts-pay.md new file mode 100644 index 000000000..a368f5cfe --- /dev/null +++ b/.changeset/new-facts-pay.md @@ -0,0 +1,5 @@ +--- +'@gitbook/integration-github-issues': patch +--- + +Add GitHub issues integration diff --git a/bun.lock b/bun.lock index 8c44fdd8f..623624e81 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "@gitbook/integrations", @@ -114,7 +113,7 @@ }, "integrations/formspree": { "name": "@gitbook/integration-formspree", - "version": "0.2.13", + "version": "0.2.14", "dependencies": { "@gitbook/runtime": "workspace:*", }, @@ -161,7 +160,7 @@ }, "integrations/github": { "name": "@gitbook/integration-github", - "version": "0.6.4", + "version": "0.6.5", "dependencies": { "@gitbook/api": "*", "@gitbook/runtime": "*", @@ -209,9 +208,25 @@ "@gitbook/tsconfig": "workspace:*", }, }, + "integrations/github-issues": { + "name": "@gitbook/integration-github-issues", + "version": "0.0.1", + "dependencies": { + "@gitbook/api": "*", + "@gitbook/runtime": "*", + "@tsndr/cloudflare-worker-jwt": "^3.2.0", + "itty-router": "^2.6.1", + "octokit": "^5.0.5", + "p-map": "^7.0.4", + }, + "devDependencies": { + "@gitbook/cli": "workspace:*", + "@gitbook/tsconfig": "workspace:*", + }, + }, "integrations/gitlab": { "name": "@gitbook/integration-gitlab", - "version": "0.6.3", + "version": "0.6.4", "dependencies": { "@gitbook/api": "*", "@gitbook/runtime": "*", @@ -314,7 +329,7 @@ }, "integrations/intercom-conversations": { "name": "@gitbook/integration-intercom-conversations", - "version": "0.2.2", + "version": "0.2.3", "dependencies": { "@gitbook/api": "*", "@gitbook/runtime": "*", @@ -445,7 +460,7 @@ }, "integrations/oidc": { "name": "@gitbook/integration-oidc", - "version": "0.2.6", + "version": "0.2.7", "dependencies": { "@gitbook/api": "*", "@gitbook/runtime": "*", @@ -730,7 +745,7 @@ }, "packages/api": { "name": "@gitbook/api", - "version": "0.152.0", + "version": "0.153.0", "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0", @@ -745,7 +760,7 @@ }, "packages/cli": { "name": "@gitbook/cli", - "version": "0.26.2", + "version": "0.26.3", "bin": { "gitbook": "./cli.js", }, @@ -785,7 +800,7 @@ }, "packages/runtime": { "name": "@gitbook/runtime", - "version": "0.23.0", + "version": "0.23.1", "dependencies": { "@gitbook/api": "*", }, @@ -1093,6 +1108,8 @@ "@gitbook/integration-github-files": ["@gitbook/integration-github-files@workspace:integrations/github-files"], + "@gitbook/integration-github-issues": ["@gitbook/integration-github-issues@workspace:integrations/github-issues"], + "@gitbook/integration-gitlab": ["@gitbook/integration-gitlab@workspace:integrations/gitlab"], "@gitbook/integration-gitlab-files": ["@gitbook/integration-gitlab-files@workspace:integrations/gitlab-files"], @@ -1383,7 +1400,7 @@ "@octokit/openapi-types": ["@octokit/openapi-types@23.0.1", "", {}, "sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g=="], - "@octokit/openapi-webhooks-types": ["@octokit/openapi-webhooks-types@10.1.1", "", {}, "sha512-qBfqQVIDQaCFeGCofXieJDwvXcGgDn17+UwZ6WW6lfEvGYGreLFzTiaz9xjet9Us4zDf8iasoW3ixUj/R5lMhA=="], + "@octokit/openapi-webhooks-types": ["@octokit/openapi-webhooks-types@12.1.0", "", {}, "sha512-WiuzhOsiOvb7W3Pvmhf8d2C6qaLHXrWiLBP4nJ/4kydu+wpagV5Fkz9RfQwV2afYzv3PB+3xYgp4mAdNGjDprA=="], "@octokit/plugin-paginate-graphql": ["@octokit/plugin-paginate-graphql@5.2.4", "", { "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-pLZES1jWaOynXKHOqdnwZ5ULeVR6tVVCMm+AUbp0htdcyXDU95WbkYdU4R2ej1wKj5Tu94Mee2Ne0PjPO9cCyA=="], @@ -1401,9 +1418,9 @@ "@octokit/types": ["@octokit/types@13.8.0", "", { "dependencies": { "@octokit/openapi-types": "^23.0.1" } }, "sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A=="], - "@octokit/webhooks": ["@octokit/webhooks@13.7.4", "", { "dependencies": { "@octokit/openapi-webhooks-types": "10.1.1", "@octokit/request-error": "^6.1.7", "@octokit/webhooks-methods": "^5.1.1" } }, "sha512-f386XyLTieQbgKPKS6ZMlH4dq8eLsxNddwofiKRZCq0bZ2gikoFwMD99K6l1oAwqe/KZNzrEziGicRgnzplplQ=="], + "@octokit/webhooks": ["@octokit/webhooks@14.2.0", "", { "dependencies": { "@octokit/openapi-webhooks-types": "12.1.0", "@octokit/request-error": "^7.0.0", "@octokit/webhooks-methods": "^6.0.0" } }, "sha512-da6KbdNCV5sr1/txD896V+6W0iamFWrvVl8cHkBSPT+YlvmT3DwXa4jxZnQc+gnuTEqSWbBeoSZYTayXH9wXcw=="], - "@octokit/webhooks-methods": ["@octokit/webhooks-methods@5.1.1", "", {}, "sha512-NGlEHZDseJTCj8TMMFehzwa9g7On4KJMPVHDSrHxCQumL6uSQR8wIkP/qesv52fXqV1BPf4pTxwtS31ldAt9Xg=="], + "@octokit/webhooks-methods": ["@octokit/webhooks-methods@6.0.0", "", {}, "sha512-MFlzzoDJVw/GcbfzVC1RLR36QqkTLUf79vLVO3D+xn7r0QgxnFoLZgtrzxiQErAjFUOdH6fas2KeQJ1yr/qaXQ=="], "@octokit/webhooks-types": ["@octokit/webhooks-types@7.6.1", "", {}, "sha512-S8u2cJzklBC0FgTwWVLaM8tMrDuDMVE4xiTK4EYXM9GntyvrdbSoxqDQa+Fh57CCNApyIpyeqPhhFEmHPfrXgw=="], @@ -2209,7 +2226,7 @@ "p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], - "p-map": ["p-map@7.0.3", "", {}, "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA=="], + "p-map": ["p-map@7.0.4", "", {}, "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ=="], "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], @@ -2677,12 +2694,22 @@ "@gitbook/integration-github-files/itty-router": ["itty-router@3.0.12", "", {}, "sha512-s98XTPhle6GGbaFf0kYrOD3Q8gyhnqvOqkwYijC3AmkceNKqWUp13YHg6dWmqmVv4pP7l7c94XI92I0EXVGO0w=="], + "@gitbook/integration-github-issues/@tsndr/cloudflare-worker-jwt": ["@tsndr/cloudflare-worker-jwt@3.2.0", "", {}, "sha512-y45452JzKxFDfCUHNGrdgIcrJTkYa6xtrKtCQTjKj+hjzw8dOHF9R0uGuU8WxvCCMi4sMzZ1KREnZTsTy7DsiQ=="], + + "@gitbook/integration-github-issues/itty-router": ["itty-router@2.6.6", "", {}, "sha512-hIPHtXGymCX7Lzb2I4G6JgZFE4QEEQwst9GORK7sMYUpJvLfy4yZJr95r04e8DzoAnj6HcxM2m4TbK+juu+18g=="], + + "@gitbook/integration-github-issues/octokit": ["octokit@5.0.5", "", { "dependencies": { "@octokit/app": "^16.1.2", "@octokit/core": "^7.0.6", "@octokit/oauth-app": "^8.0.3", "@octokit/plugin-paginate-graphql": "^6.0.0", "@octokit/plugin-paginate-rest": "^14.0.0", "@octokit/plugin-rest-endpoint-methods": "^17.0.0", "@octokit/plugin-retry": "^8.0.3", "@octokit/plugin-throttling": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "@octokit/webhooks": "^14.0.0" } }, "sha512-4+/OFSqOjoyULo7eN7EA97DE0Xydj/PW5aIckxqQIoFjFwqXKuFCvXUJObyJfBF9Khu4RL/jlDRI9FPaMGfPnw=="], + "@gitbook/integration-gitlab/itty-router": ["itty-router@4.2.2", "", {}, "sha512-KegPW0l9SNPadProoFT07AB84uOqLUwzlXQ7HsqkS31WUrxkjdhcemRpTDUuetbMJ89uBtWeQSVoiEmUAu31uw=="], "@gitbook/integration-hubspot-conversations/itty-router": ["itty-router@2.6.6", "", {}, "sha512-hIPHtXGymCX7Lzb2I4G6JgZFE4QEEQwst9GORK7sMYUpJvLfy4yZJr95r04e8DzoAnj6HcxM2m4TbK+juu+18g=="], + "@gitbook/integration-hubspot-conversations/p-map": ["p-map@7.0.3", "", {}, "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA=="], + "@gitbook/integration-intercom-conversations/itty-router": ["itty-router@4.2.2", "", {}, "sha512-KegPW0l9SNPadProoFT07AB84uOqLUwzlXQ7HsqkS31WUrxkjdhcemRpTDUuetbMJ89uBtWeQSVoiEmUAu31uw=="], + "@gitbook/integration-intercom-conversations/p-map": ["p-map@7.0.3", "", {}, "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA=="], + "@gitbook/integration-jira/itty-router": ["itty-router@2.6.6", "", {}, "sha512-hIPHtXGymCX7Lzb2I4G6JgZFE4QEEQwst9GORK7sMYUpJvLfy4yZJr95r04e8DzoAnj6HcxM2m4TbK+juu+18g=="], "@gitbook/integration-lucid/itty-router": ["itty-router@4.2.2", "", {}, "sha512-KegPW0l9SNPadProoFT07AB84uOqLUwzlXQ7HsqkS31WUrxkjdhcemRpTDUuetbMJ89uBtWeQSVoiEmUAu31uw=="], @@ -2711,6 +2738,8 @@ "@gitbook/integration-va-okta/itty-router": ["itty-router@4.2.2", "", {}, "sha512-KegPW0l9SNPadProoFT07AB84uOqLUwzlXQ7HsqkS31WUrxkjdhcemRpTDUuetbMJ89uBtWeQSVoiEmUAu31uw=="], + "@gitbook/integration-zendesk-conversations/p-map": ["p-map@7.0.3", "", {}, "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA=="], + "@gitbook/runtime/esbuild": ["esbuild@0.14.54", "", { "optionalDependencies": { "@esbuild/linux-loong64": "0.14.54", "esbuild-android-64": "0.14.54", "esbuild-android-arm64": "0.14.54", "esbuild-darwin-64": "0.14.54", "esbuild-darwin-arm64": "0.14.54", "esbuild-freebsd-64": "0.14.54", "esbuild-freebsd-arm64": "0.14.54", "esbuild-linux-32": "0.14.54", "esbuild-linux-64": "0.14.54", "esbuild-linux-arm": "0.14.54", "esbuild-linux-arm64": "0.14.54", "esbuild-linux-mips64le": "0.14.54", "esbuild-linux-ppc64le": "0.14.54", "esbuild-linux-riscv64": "0.14.54", "esbuild-linux-s390x": "0.14.54", "esbuild-netbsd-64": "0.14.54", "esbuild-openbsd-64": "0.14.54", "esbuild-sunos-64": "0.14.54", "esbuild-windows-32": "0.14.54", "esbuild-windows-64": "0.14.54", "esbuild-windows-arm64": "0.14.54" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA=="], "@graphql-codegen/cli/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -2799,8 +2828,12 @@ "@octokit/action/undici": ["undici@6.21.2", "", {}, "sha512-uROZWze0R0itiAKVPsYhFov9LxrPMHLMEQFszeI2gCN6bnIIZ8twzBCJcN2LJrBBLfrP0t1FW0g+JmKVl8Vk1g=="], + "@octokit/app/@octokit/webhooks": ["@octokit/webhooks@13.7.4", "", { "dependencies": { "@octokit/openapi-webhooks-types": "10.1.1", "@octokit/request-error": "^6.1.7", "@octokit/webhooks-methods": "^5.1.1" } }, "sha512-f386XyLTieQbgKPKS6ZMlH4dq8eLsxNddwofiKRZCq0bZ2gikoFwMD99K6l1oAwqe/KZNzrEziGicRgnzplplQ=="], + "@octokit/auth-action/@octokit/auth-token": ["@octokit/auth-token@4.0.0", "", {}, "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA=="], + "@octokit/webhooks/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], + "@sinonjs/commons/type-detect": ["type-detect@4.0.8", "", {}, "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g=="], "@whatwg-node/disposablestack/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -3009,6 +3042,26 @@ "@gitbook/integration-front/@gitbook/cli/esbuild": ["esbuild@0.17.19", "", { "optionalDependencies": { "@esbuild/android-arm": "0.17.19", "@esbuild/android-arm64": "0.17.19", "@esbuild/android-x64": "0.17.19", "@esbuild/darwin-arm64": "0.17.19", "@esbuild/darwin-x64": "0.17.19", "@esbuild/freebsd-arm64": "0.17.19", "@esbuild/freebsd-x64": "0.17.19", "@esbuild/linux-arm": "0.17.19", "@esbuild/linux-arm64": "0.17.19", "@esbuild/linux-ia32": "0.17.19", "@esbuild/linux-loong64": "0.17.19", "@esbuild/linux-mips64el": "0.17.19", "@esbuild/linux-ppc64": "0.17.19", "@esbuild/linux-riscv64": "0.17.19", "@esbuild/linux-s390x": "0.17.19", "@esbuild/linux-x64": "0.17.19", "@esbuild/netbsd-x64": "0.17.19", "@esbuild/openbsd-x64": "0.17.19", "@esbuild/sunos-x64": "0.17.19", "@esbuild/win32-arm64": "0.17.19", "@esbuild/win32-ia32": "0.17.19", "@esbuild/win32-x64": "0.17.19" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw=="], + "@gitbook/integration-github-issues/octokit/@octokit/app": ["@octokit/app@16.1.2", "", { "dependencies": { "@octokit/auth-app": "^8.1.2", "@octokit/auth-unauthenticated": "^7.0.3", "@octokit/core": "^7.0.6", "@octokit/oauth-app": "^8.0.3", "@octokit/plugin-paginate-rest": "^14.0.0", "@octokit/types": "^16.0.0", "@octokit/webhooks": "^14.0.0" } }, "sha512-8j7sEpUYVj18dxvh0KWj6W/l6uAiVRBl1JBDVRqH1VHKAO/G5eRVl4yEoYACjakWers1DjUkcCHyJNQK47JqyQ=="], + + "@gitbook/integration-github-issues/octokit/@octokit/core": ["@octokit/core@7.0.6", "", { "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", "@octokit/request": "^10.0.6", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "before-after-hook": "^4.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q=="], + + "@gitbook/integration-github-issues/octokit/@octokit/oauth-app": ["@octokit/oauth-app@8.0.3", "", { "dependencies": { "@octokit/auth-oauth-app": "^9.0.2", "@octokit/auth-oauth-user": "^6.0.1", "@octokit/auth-unauthenticated": "^7.0.2", "@octokit/core": "^7.0.5", "@octokit/oauth-authorization-url": "^8.0.0", "@octokit/oauth-methods": "^6.0.1", "@types/aws-lambda": "^8.10.83", "universal-user-agent": "^7.0.0" } }, "sha512-jnAjvTsPepyUaMu9e69hYBuozEPgYqP4Z3UnpmvoIzHDpf8EXDGvTY1l1jK0RsZ194oRd+k6Hm13oRU8EoDFwg=="], + + "@gitbook/integration-github-issues/octokit/@octokit/plugin-paginate-graphql": ["@octokit/plugin-paginate-graphql@6.0.0", "", { "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-crfpnIoFiBtRkvPqOyLOsw12XsveYuY2ieP6uYDosoUegBJpSVxGwut9sxUgFFcll3VTOTqpUf8yGd8x1OmAkQ=="], + + "@gitbook/integration-github-issues/octokit/@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@14.0.0", "", { "dependencies": { "@octokit/types": "^16.0.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw=="], + + "@gitbook/integration-github-issues/octokit/@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@17.0.0", "", { "dependencies": { "@octokit/types": "^16.0.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw=="], + + "@gitbook/integration-github-issues/octokit/@octokit/plugin-retry": ["@octokit/plugin-retry@8.0.3", "", { "dependencies": { "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "bottleneck": "^2.15.3" }, "peerDependencies": { "@octokit/core": ">=7" } }, "sha512-vKGx1i3MC0za53IzYBSBXcrhmd+daQDzuZfYDd52X5S0M2otf3kVZTVP8bLA3EkU0lTvd1WEC2OlNNa4G+dohA=="], + + "@gitbook/integration-github-issues/octokit/@octokit/plugin-throttling": ["@octokit/plugin-throttling@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "bottleneck": "^2.15.3" }, "peerDependencies": { "@octokit/core": "^7.0.0" } }, "sha512-34eE0RkFCKycLl2D2kq7W+LovheM/ex3AwZCYN8udpi6bxsyjZidb2McXs69hZhLmJlDqTSP8cH+jSRpiaijBg=="], + + "@gitbook/integration-github-issues/octokit/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], + + "@gitbook/integration-github-issues/octokit/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], + "@gitbook/integration-marketo/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.15.18", "", { "os": "android", "cpu": "arm" }, "sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw=="], "@gitbook/integration-marketo/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.15.18", "", { "os": "linux", "cpu": "none" }, "sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ=="], @@ -3135,6 +3188,12 @@ "@octokit/action/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="], + "@octokit/app/@octokit/webhooks/@octokit/openapi-webhooks-types": ["@octokit/openapi-webhooks-types@10.1.1", "", {}, "sha512-qBfqQVIDQaCFeGCofXieJDwvXcGgDn17+UwZ6WW6lfEvGYGreLFzTiaz9xjet9Us4zDf8iasoW3ixUj/R5lMhA=="], + + "@octokit/app/@octokit/webhooks/@octokit/webhooks-methods": ["@octokit/webhooks-methods@5.1.1", "", {}, "sha512-NGlEHZDseJTCj8TMMFehzwa9g7On4KJMPVHDSrHxCQumL6uSQR8wIkP/qesv52fXqV1BPf4pTxwtS31ldAt9Xg=="], + + "@octokit/webhooks/@octokit/request-error/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], + "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "enquirer/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -3263,6 +3322,30 @@ "@gitbook/integration-front/@gitbook/cli/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.17.19", "", { "os": "win32", "cpu": "x64" }, "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA=="], + "@gitbook/integration-github-issues/octokit/@octokit/app/@octokit/auth-app": ["@octokit/auth-app@8.1.2", "", { "dependencies": { "@octokit/auth-oauth-app": "^9.0.3", "@octokit/auth-oauth-user": "^6.0.2", "@octokit/request": "^10.0.6", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "toad-cache": "^3.7.0", "universal-github-app-jwt": "^2.2.0", "universal-user-agent": "^7.0.0" } }, "sha512-db8VO0PqXxfzI6GdjtgEFHY9tzqUql5xMFXYA12juq8TeTgPAuiiP3zid4h50lwlIP457p5+56PnJOgd2GGBuw=="], + + "@gitbook/integration-github-issues/octokit/@octokit/app/@octokit/auth-unauthenticated": ["@octokit/auth-unauthenticated@7.0.3", "", { "dependencies": { "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0" } }, "sha512-8Jb1mtUdmBHL7lGmop9mU9ArMRUTRhg8vp0T1VtZ4yd9vEm3zcLwmjQkhNEduKawOOORie61xhtYIhTDN+ZQ3g=="], + + "@gitbook/integration-github-issues/octokit/@octokit/core/@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="], + + "@gitbook/integration-github-issues/octokit/@octokit/core/@octokit/graphql": ["@octokit/graphql@9.0.3", "", { "dependencies": { "@octokit/request": "^10.0.6", "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA=="], + + "@gitbook/integration-github-issues/octokit/@octokit/core/@octokit/request": ["@octokit/request@10.0.7", "", { "dependencies": { "@octokit/endpoint": "^11.0.2", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA=="], + + "@gitbook/integration-github-issues/octokit/@octokit/core/before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="], + + "@gitbook/integration-github-issues/octokit/@octokit/oauth-app/@octokit/auth-oauth-app": ["@octokit/auth-oauth-app@9.0.3", "", { "dependencies": { "@octokit/auth-oauth-device": "^8.0.3", "@octokit/auth-oauth-user": "^6.0.2", "@octokit/request": "^10.0.6", "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-+yoFQquaF8OxJSxTb7rnytBIC2ZLbLqA/yb71I4ZXT9+Slw4TziV9j/kyGhUFRRTF2+7WlnIWsePZCWHs+OGjg=="], + + "@gitbook/integration-github-issues/octokit/@octokit/oauth-app/@octokit/auth-oauth-user": ["@octokit/auth-oauth-user@6.0.2", "", { "dependencies": { "@octokit/auth-oauth-device": "^8.0.3", "@octokit/oauth-methods": "^6.0.2", "@octokit/request": "^10.0.6", "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-qLoPPc6E6GJoz3XeDG/pnDhJpTkODTGG4kY0/Py154i/I003O9NazkrwJwRuzgCalhzyIeWQ+6MDvkUmKXjg/A=="], + + "@gitbook/integration-github-issues/octokit/@octokit/oauth-app/@octokit/auth-unauthenticated": ["@octokit/auth-unauthenticated@7.0.3", "", { "dependencies": { "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0" } }, "sha512-8Jb1mtUdmBHL7lGmop9mU9ArMRUTRhg8vp0T1VtZ4yd9vEm3zcLwmjQkhNEduKawOOORie61xhtYIhTDN+ZQ3g=="], + + "@gitbook/integration-github-issues/octokit/@octokit/oauth-app/@octokit/oauth-authorization-url": ["@octokit/oauth-authorization-url@8.0.0", "", {}, "sha512-7QoLPRh/ssEA/HuHBHdVdSgF8xNLz/Bc5m9fZkArJE5bb6NmVkDm3anKxXPmN1zh6b5WKZPRr3697xKT/yM3qQ=="], + + "@gitbook/integration-github-issues/octokit/@octokit/oauth-app/@octokit/oauth-methods": ["@octokit/oauth-methods@6.0.2", "", { "dependencies": { "@octokit/oauth-authorization-url": "^8.0.0", "@octokit/request": "^10.0.6", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0" } }, "sha512-HiNOO3MqLxlt5Da5bZbLV8Zarnphi4y9XehrbaFMkcoJ+FL7sMxH/UlUsCVxpddVu4qvNDrBdaTVE2o4ITK8ng=="], + + "@gitbook/integration-github-issues/octokit/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], + "@graphql-codegen/typescript-graphql-request/@graphql-codegen/plugin-helpers/@graphql-tools/utils/tslib": ["tslib@2.6.3", "", {}, "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="], "@graphql-codegen/typescript-graphql-request/@graphql-codegen/visitor-plugin-common/@graphql-tools/optimize/tslib": ["tslib@2.6.3", "", {}, "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="], @@ -3277,6 +3360,8 @@ "@octokit/action/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@9.0.6", "", { "dependencies": { "@octokit/types": "^13.1.0", "universal-user-agent": "^6.0.0" } }, "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw=="], + "@octokit/webhooks/@octokit/request-error/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], + "inquirer/cli-cursor/restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], "inquirer/ora/bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], @@ -3291,18 +3376,66 @@ "@apidevtools/swagger-cli/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "@gitbook/integration-github-issues/octokit/@octokit/app/@octokit/auth-app/@octokit/auth-oauth-app": ["@octokit/auth-oauth-app@9.0.3", "", { "dependencies": { "@octokit/auth-oauth-device": "^8.0.3", "@octokit/auth-oauth-user": "^6.0.2", "@octokit/request": "^10.0.6", "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-+yoFQquaF8OxJSxTb7rnytBIC2ZLbLqA/yb71I4ZXT9+Slw4TziV9j/kyGhUFRRTF2+7WlnIWsePZCWHs+OGjg=="], + + "@gitbook/integration-github-issues/octokit/@octokit/app/@octokit/auth-app/@octokit/auth-oauth-user": ["@octokit/auth-oauth-user@6.0.2", "", { "dependencies": { "@octokit/auth-oauth-device": "^8.0.3", "@octokit/oauth-methods": "^6.0.2", "@octokit/request": "^10.0.6", "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-qLoPPc6E6GJoz3XeDG/pnDhJpTkODTGG4kY0/Py154i/I003O9NazkrwJwRuzgCalhzyIeWQ+6MDvkUmKXjg/A=="], + + "@gitbook/integration-github-issues/octokit/@octokit/app/@octokit/auth-app/@octokit/request": ["@octokit/request@10.0.7", "", { "dependencies": { "@octokit/endpoint": "^11.0.2", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA=="], + + "@gitbook/integration-github-issues/octokit/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.2", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ=="], + + "@gitbook/integration-github-issues/octokit/@octokit/core/@octokit/request/fast-content-type-parse": ["fast-content-type-parse@3.0.0", "", {}, "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg=="], + + "@gitbook/integration-github-issues/octokit/@octokit/oauth-app/@octokit/auth-oauth-app/@octokit/auth-oauth-device": ["@octokit/auth-oauth-device@8.0.3", "", { "dependencies": { "@octokit/oauth-methods": "^6.0.2", "@octokit/request": "^10.0.6", "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-zh2W0mKKMh/VWZhSqlaCzY7qFyrgd9oTWmTmHaXnHNeQRCZr/CXy2jCgHo4e4dJVTiuxP5dLa0YM5p5QVhJHbw=="], + + "@gitbook/integration-github-issues/octokit/@octokit/oauth-app/@octokit/auth-oauth-app/@octokit/request": ["@octokit/request@10.0.7", "", { "dependencies": { "@octokit/endpoint": "^11.0.2", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA=="], + + "@gitbook/integration-github-issues/octokit/@octokit/oauth-app/@octokit/auth-oauth-user/@octokit/auth-oauth-device": ["@octokit/auth-oauth-device@8.0.3", "", { "dependencies": { "@octokit/oauth-methods": "^6.0.2", "@octokit/request": "^10.0.6", "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-zh2W0mKKMh/VWZhSqlaCzY7qFyrgd9oTWmTmHaXnHNeQRCZr/CXy2jCgHo4e4dJVTiuxP5dLa0YM5p5QVhJHbw=="], + + "@gitbook/integration-github-issues/octokit/@octokit/oauth-app/@octokit/auth-oauth-user/@octokit/request": ["@octokit/request@10.0.7", "", { "dependencies": { "@octokit/endpoint": "^11.0.2", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA=="], + + "@gitbook/integration-github-issues/octokit/@octokit/oauth-app/@octokit/oauth-methods/@octokit/request": ["@octokit/request@10.0.7", "", { "dependencies": { "@octokit/endpoint": "^11.0.2", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA=="], + "@graphql-codegen/typescript-graphql-request/@graphql-codegen/visitor-plugin-common/@graphql-tools/relay-operation-optimizer/@ardatan/relay-compiler/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "@graphql-codegen/typescript-graphql-request/@graphql-codegen/visitor-plugin-common/@graphql-tools/relay-operation-optimizer/@ardatan/relay-compiler/yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="], + "@gitbook/integration-github-issues/octokit/@octokit/app/@octokit/auth-app/@octokit/auth-oauth-app/@octokit/auth-oauth-device": ["@octokit/auth-oauth-device@8.0.3", "", { "dependencies": { "@octokit/oauth-methods": "^6.0.2", "@octokit/request": "^10.0.6", "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-zh2W0mKKMh/VWZhSqlaCzY7qFyrgd9oTWmTmHaXnHNeQRCZr/CXy2jCgHo4e4dJVTiuxP5dLa0YM5p5QVhJHbw=="], + + "@gitbook/integration-github-issues/octokit/@octokit/app/@octokit/auth-app/@octokit/auth-oauth-user/@octokit/auth-oauth-device": ["@octokit/auth-oauth-device@8.0.3", "", { "dependencies": { "@octokit/oauth-methods": "^6.0.2", "@octokit/request": "^10.0.6", "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-zh2W0mKKMh/VWZhSqlaCzY7qFyrgd9oTWmTmHaXnHNeQRCZr/CXy2jCgHo4e4dJVTiuxP5dLa0YM5p5QVhJHbw=="], + + "@gitbook/integration-github-issues/octokit/@octokit/app/@octokit/auth-app/@octokit/auth-oauth-user/@octokit/oauth-methods": ["@octokit/oauth-methods@6.0.2", "", { "dependencies": { "@octokit/oauth-authorization-url": "^8.0.0", "@octokit/request": "^10.0.6", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0" } }, "sha512-HiNOO3MqLxlt5Da5bZbLV8Zarnphi4y9XehrbaFMkcoJ+FL7sMxH/UlUsCVxpddVu4qvNDrBdaTVE2o4ITK8ng=="], + + "@gitbook/integration-github-issues/octokit/@octokit/app/@octokit/auth-app/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.2", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ=="], + + "@gitbook/integration-github-issues/octokit/@octokit/app/@octokit/auth-app/@octokit/request/fast-content-type-parse": ["fast-content-type-parse@3.0.0", "", {}, "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg=="], + + "@gitbook/integration-github-issues/octokit/@octokit/oauth-app/@octokit/auth-oauth-app/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.2", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ=="], + + "@gitbook/integration-github-issues/octokit/@octokit/oauth-app/@octokit/auth-oauth-app/@octokit/request/fast-content-type-parse": ["fast-content-type-parse@3.0.0", "", {}, "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg=="], + + "@gitbook/integration-github-issues/octokit/@octokit/oauth-app/@octokit/auth-oauth-user/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.2", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ=="], + + "@gitbook/integration-github-issues/octokit/@octokit/oauth-app/@octokit/auth-oauth-user/@octokit/request/fast-content-type-parse": ["fast-content-type-parse@3.0.0", "", {}, "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg=="], + + "@gitbook/integration-github-issues/octokit/@octokit/oauth-app/@octokit/oauth-methods/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.2", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ=="], + + "@gitbook/integration-github-issues/octokit/@octokit/oauth-app/@octokit/oauth-methods/@octokit/request/fast-content-type-parse": ["fast-content-type-parse@3.0.0", "", {}, "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg=="], + "@graphql-codegen/typescript-graphql-request/@graphql-codegen/visitor-plugin-common/@graphql-tools/relay-operation-optimizer/@ardatan/relay-compiler/yargs/cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], "@graphql-codegen/typescript-graphql-request/@graphql-codegen/visitor-plugin-common/@graphql-tools/relay-operation-optimizer/@ardatan/relay-compiler/yargs/y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="], "@graphql-codegen/typescript-graphql-request/@graphql-codegen/visitor-plugin-common/@graphql-tools/relay-operation-optimizer/@ardatan/relay-compiler/yargs/yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="], + "@gitbook/integration-github-issues/octokit/@octokit/app/@octokit/auth-app/@octokit/auth-oauth-app/@octokit/auth-oauth-device/@octokit/oauth-methods": ["@octokit/oauth-methods@6.0.2", "", { "dependencies": { "@octokit/oauth-authorization-url": "^8.0.0", "@octokit/request": "^10.0.6", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0" } }, "sha512-HiNOO3MqLxlt5Da5bZbLV8Zarnphi4y9XehrbaFMkcoJ+FL7sMxH/UlUsCVxpddVu4qvNDrBdaTVE2o4ITK8ng=="], + + "@gitbook/integration-github-issues/octokit/@octokit/app/@octokit/auth-app/@octokit/auth-oauth-user/@octokit/oauth-methods/@octokit/oauth-authorization-url": ["@octokit/oauth-authorization-url@8.0.0", "", {}, "sha512-7QoLPRh/ssEA/HuHBHdVdSgF8xNLz/Bc5m9fZkArJE5bb6NmVkDm3anKxXPmN1zh6b5WKZPRr3697xKT/yM3qQ=="], + "@graphql-codegen/typescript-graphql-request/@graphql-codegen/visitor-plugin-common/@graphql-tools/relay-operation-optimizer/@ardatan/relay-compiler/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "@gitbook/integration-github-issues/octokit/@octokit/app/@octokit/auth-app/@octokit/auth-oauth-app/@octokit/auth-oauth-device/@octokit/oauth-methods/@octokit/oauth-authorization-url": ["@octokit/oauth-authorization-url@8.0.0", "", {}, "sha512-7QoLPRh/ssEA/HuHBHdVdSgF8xNLz/Bc5m9fZkArJE5bb6NmVkDm3anKxXPmN1zh6b5WKZPRr3697xKT/yM3qQ=="], + "@graphql-codegen/typescript-graphql-request/@graphql-codegen/visitor-plugin-common/@graphql-tools/relay-operation-optimizer/@ardatan/relay-compiler/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], } } diff --git a/integrations/github-issues/assets/icon.png b/integrations/github-issues/assets/icon.png new file mode 100644 index 000000000..6cb3b705d Binary files /dev/null and b/integrations/github-issues/assets/icon.png differ diff --git a/integrations/github-issues/gitbook-manifest.yaml b/integrations/github-issues/gitbook-manifest.yaml new file mode 100644 index 000000000..254169eef --- /dev/null +++ b/integrations/github-issues/gitbook-manifest.yaml @@ -0,0 +1,52 @@ +name: github-issues +title: GitHub Issues +icon: ./assets/icon.png +description: Automatically sync GitHub issues to docs updates in GitBook. +visibility: public +script: ./src/index.ts +summary: | + # Overview + + Automatically get AI-suggested change requests for your docs based on feedback from your GitHub Issues. +scopes: + - conversations:ingest +organization: gitbook +configurations: + account: + componentId: config +target: organization +envs: + dev-steeve: + organization: idE5kUnGGjoPGcbu3FZJ + secrets: + GITHUB_APP_ID: ${{ "op://gitbook-integrations/githubIssuesDevSteeve/GITHUB_APP_ID" }} + GITHUB_APP_NAME: ${{ "op://gitbook-integrations/githubIssuesDevSteeve/GITHUB_APP_NAME" }} + GITHUB_PRIVATE_KEY: ${{ "op://gitbook-integrations/githubIssuesDevSteeve/GITHUB_PRIVATE_KEY" }} + CLIENT_ID: ${{ "op://gitbook-integrations/githubIssuesDevSteeve/CLIENT_ID" }} + CLIENT_SECRET: ${{ "op://gitbook-integrations/githubIssuesDevSteeve/CLIENT_SECRET" }} + WEBHOOK_SECRET: ${{ "op://gitbook-integrations/githubIssuesDevSteeve/WEBHOOK_SECRET" }} + test: + secrets: + GITHUB_APP_ID: ${{ "op://gitbook-integrations/githubIssuesStaging/GITHUB_APP_ID" }} + GITHUB_APP_NAME: ${{ "op://gitbook-integrations/githubIssuesStaging/GITHUB_APP_NAME" }} + GITHUB_PRIVATE_KEY: ${{ "op://gitbook-integrations/githubIssuesStaging/GITHUB_PRIVATE_KEY" }} + CLIENT_ID: ${{ "op://gitbook-integrations/githubIssuesStaging/CLIENT_ID" }} + CLIENT_SECRET: ${{ "op://gitbook-integrations/githubIssuesStaging/CLIENT_SECRET" }} + WEBHOOK_SECRET: ${{ "op://gitbook-integrations/githubIssuesStaging/WEBHOOK_SECRET" }} + staging: + secrets: + GITHUB_APP_ID: ${{ "op://gitbook-integrations/githubIssuesStaging/GITHUB_APP_ID" }} + GITHUB_APP_NAME: ${{ "op://gitbook-integrations/githubIssuesStaging/GITHUB_APP_NAME" }} + GITHUB_PRIVATE_KEY: ${{ "op://gitbook-integrations/githubIssuesStaging/GITHUB_PRIVATE_KEY" }} + CLIENT_ID: ${{ "op://gitbook-integrations/githubIssuesStaging/CLIENT_ID" }} + CLIENT_SECRET: ${{ "op://gitbook-integrations/githubIssuesStaging/CLIENT_SECRET" }} + WEBHOOK_SECRET: ${{ "op://gitbook-integrations/githubIssuesStaging/WEBHOOK_SECRET" }} + production: + visibility: unlisted + secrets: + GITHUB_APP_ID: ${{ "op://gitbook-integrations/githubIssuesProd/GITHUB_APP_ID" }} + GITHUB_APP_NAME: ${{ "op://gitbook-integrations/githubIssuesProd/GITHUB_APP_NAME" }} + GITHUB_PRIVATE_KEY: ${{ "op://gitbook-integrations/githubIssuesProd/GITHUB_PRIVATE_KEY" }} + CLIENT_ID: ${{ "op://gitbook-integrations/githubIssuesProd/CLIENT_ID" }} + CLIENT_SECRET: ${{ "op://gitbook-integrations/githubIssuesProd/CLIENT_SECRET" }} + WEBHOOK_SECRET: ${{ "op://gitbook-integrations/githubIssuesProd/WEBHOOK_SECRET" }} diff --git a/integrations/github-issues/package.json b/integrations/github-issues/package.json new file mode 100644 index 000000000..53d4e263f --- /dev/null +++ b/integrations/github-issues/package.json @@ -0,0 +1,23 @@ +{ + "name": "@gitbook/integration-github-issues", + "version": "0.0.1", + "private": true, + "dependencies": { + "@gitbook/runtime": "*", + "@gitbook/api": "*", + "itty-router": "^2.6.1", + "octokit": "^5.0.5", + "p-map": "^7.0.4", + "@tsndr/cloudflare-worker-jwt": "^3.2.0" + }, + "devDependencies": { + "@gitbook/cli": "workspace:*", + "@gitbook/tsconfig": "workspace:*" + }, + "scripts": { + "typecheck": "tsc --noEmit", + "check": "gitbook check", + "publish-integrations": "gitbook publish .", + "publish-integrations-staging": "gitbook publish . --env staging" + } +} diff --git a/integrations/github-issues/src/components.tsx b/integrations/github-issues/src/components.tsx new file mode 100644 index 000000000..a8d41516d --- /dev/null +++ b/integrations/github-issues/src/components.tsx @@ -0,0 +1,53 @@ +import { createComponent, InstallationConfigurationProps } from '@gitbook/runtime'; +import type { GitHubIssuesRuntimeContext, GitHubIssuesRuntimeEnvironment } from './types'; +import { createGitHubAppSetupState } from './setup'; + +/** + * Configuration component for the GitHub Issues integration. + */ +export const configComponent = createComponent< + InstallationConfigurationProps, + {}, + undefined, + GitHubIssuesRuntimeContext +>({ + componentId: 'config', + render: async (element, context) => { + const { installation } = context.environment; + + if (!installation) { + return null; + } + + const config = element.props.installation.configuration; + const hasInstallations = config.installation_ids && config.installation_ids.length > 0; + + const githubAppInstallURL = new URL( + `https://github.com/apps/${context.environment.secrets.GITHUB_APP_NAME}/installations/new`, + ); + const githubAppSetupState = await createGitHubAppSetupState(context, { + gitbookInstallationId: installation.id, + }); + githubAppInstallURL.searchParams.append('state', githubAppSetupState); + + return ( + + + } + /> + + ); + }, +}); diff --git a/integrations/github-issues/src/github-api/client.ts b/integrations/github-issues/src/github-api/client.ts new file mode 100644 index 000000000..17d865509 --- /dev/null +++ b/integrations/github-issues/src/github-api/client.ts @@ -0,0 +1,91 @@ +import jwt from '@tsndr/cloudflare-worker-jwt'; +import { Octokit } from 'octokit'; + +import { ExposableError } from '@gitbook/runtime'; +import { GitHubIssuesRuntimeContext } from '../types'; + +const GITBOOK_INTEGRATION_USER_AGENT = 'GitBook-GitHub-Issues-Integration'; + +/** + * Get an authenticated Octokit instance for a GitHub app installation. + */ +export async function getOctokitClientForInstallation( + context: GitHubIssuesRuntimeContext, + githubInstallationId: string, +): Promise { + const config = getGitHubAppConfig(context); + if (!config.appId || !config.privateKey) { + throw new ExposableError('GitHub App credentials not configured'); + } + + const token = await getGitHubInstallationAccessToken({ + githubInstallationId, + appId: config.appId, + privateKey: config.privateKey, + }); + + return new Octokit({ + auth: token, + userAgent: GITBOOK_INTEGRATION_USER_AGENT, + }); +} +/** + * Generate a JWT token for GitHub App authentication. + */ +async function generateGitHubAppJWT(appId: string, privateKey: string): Promise { + const now = Math.floor(Date.now() / 1000); + + const payload = { + iat: now - 60, // Issued 60 seconds ago (for clock drift) + exp: now + 60 * 10, + iss: appId, + }; + + return await jwt.sign(payload, privateKey, { algorithm: 'RS256' }); +} + +/** + * Get an access token for a GitHub App installation. + */ +async function getGitHubInstallationAccessToken(args: { + githubInstallationId: string; + appId: string; + privateKey: string; +}): Promise { + const { githubInstallationId, appId, privateKey } = args; + const jwtToken = await generateGitHubAppJWT(appId, privateKey); + + const octokit = new Octokit({ + auth: jwtToken, + userAgent: GITBOOK_INTEGRATION_USER_AGENT, + }); + + try { + const response = await octokit.request( + 'POST /app/installations/{installation_id}/access_tokens', + { + installation_id: parseInt(githubInstallationId), + }, + ); + + return response.data.token; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to get installation access token: ${errorMessage}`); + } +} + +/** + * Get GitHub App configuration for installation-based authentication. + */ +export function getGitHubAppConfig(context: GitHubIssuesRuntimeContext) { + // We store the private key in 1password with newlines escaped to avoid the newlines from being removed when stored as password field in the OP entry. + // This means that it will also be stored with escaped newlines in the integration secret config so we need to restore the newlines + // before we sign the JWT as we need the private key in a proper PKCS8 format. + const privateKey = context.environment.secrets.GITHUB_PRIVATE_KEY.replace(/\\n/g, '\n'); + + return { + appId: context.environment.secrets.GITHUB_APP_ID, + privateKey, + }; +} diff --git a/integrations/github-issues/src/github-api/graphql.ts b/integrations/github-issues/src/github-api/graphql.ts new file mode 100644 index 000000000..af5aa97d4 --- /dev/null +++ b/integrations/github-issues/src/github-api/graphql.ts @@ -0,0 +1,209 @@ +import type { Octokit } from 'octokit'; + +/** + * Retrieve the IDs of all issues closed in the last 30 days from a GitHub repository. + * + * Fetch only IDs for fast retrieval. + */ +export async function getGitHubRepoClosedIssueIdsLast30Days(args: { + octokit: Octokit; + repository: { + owner: string; + name: string; + }; + page?: { + after: string | null; + limit: number; + }; +}) { + const { octokit, repository, page = { after: null, limit: 50 } } = args; + + const closedSinceDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) + .toISOString() + .split('T')[0]; + + const searchQuery = `repo:${repository.owner}/${repository.name} type:issue state:closed closed:>${closedSinceDate}`; + const graphQLQuery = ` + query GetClosedIssuesLast30($searchQuery: String!, $first: Int!, $after: String) { + search(query: $searchQuery, type: ISSUE, first: $first, after: $after) { + issueCount + pageInfo { + hasNextPage + endCursor + } + nodes { + ...on Issue { + id + } + } + } + } + `; + + try { + return await octokit.graphql(graphQLQuery, { + searchQuery, + after: page.after, + first: page.limit, + }); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + throw new Error(`GitHub GraphQL API error: ${errorMessage}`); + } +} + +/** + * Retrieve a list of issues from a GitHub repository matching the list of provided IDs. + */ +export async function getGitHubRepoIssueById(args: { + octokit: Octokit; + issueId: GitHubIssue['id']; +}) { + const { octokit, issueId } = args; + + const graphQLQuery = ` + ${ISSUE_WITH_COMMENTS_FRAGMENT} + query GetIssueById($issueId: ID!) { + node(id: $issueId) { + ...IssueWithComments + } + } + `; + + try { + return await octokit.graphql(graphQLQuery, { issueId }); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + throw new Error(`GitHub GraphQL API error: ${errorMessage}`); + } +} + +/** + * Retrieve a list of issues from a GitHub repository matching the list of provided IDs. + */ +export async function getGitHubRepoIssuesByIds(args: { + octokit: Octokit; + issueIds: GitHubIssue['id'][]; +}) { + const { octokit, issueIds } = args; + + const graphQLQuery = ` + ${ISSUE_WITH_COMMENTS_FRAGMENT} + query GetIssuesByIds { + nodes(ids: ${JSON.stringify(issueIds)}) { + ...IssueWithComments + } + } + `; + + try { + return await octokit.graphql(graphQLQuery); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + throw new Error(`GitHub GraphQL API error: ${errorMessage}`); + } +} + +// +// Fragments +// +// Note: Make sure to update corresponding schema types below when updating the fragments. +// +const ISSUE_WITH_COMMENTS_FRAGMENT = ` + fragment IssueWithComments on Issue { + id + number + title + body + url + createdAt + closedAt + author { + login + } + authorAssociation + comments(last: 50) { + nodes { + body + author { + login + } + authorAssociation + } + } + repository { + name + owner { + login + } + } + } +`; + +// +// Schema types +// +// Note: Make sure to update corresponding fragments when updating the schema types. +// +interface GitHubIssuesSearchResponse { + search: { + issueCount: number; + pageInfo: { + hasNextPage: boolean; + endCursor?: string | null; + }; + nodes: IssueNodesType[]; + } | null; +} +type GetClosedIssuesLast30DaysResponse = GitHubIssuesSearchResponse<{ id: GitHubIssue['id'] }>; +interface GetIssuesByIdsResponse { + nodes: GitHubIssue[]; +} +interface GetIssueByIdResponse { + node: GitHubIssue; +} + +export interface GitHubIssue { + id: string; + number: number; + title: string; + body: string; + url: string; + createdAt: string; + closedAt: string; + + author: { + login: string; + } | null; + authorAssociation: AuthorAssociation; + + comments: { + nodes: GitHubIssueComment[]; + }; + + repository: { + name: string; + owner: { + login: string; + }; + }; +} + +export interface GitHubIssueComment { + body: string; + author: { + login: string; + } | null; + + authorAssociation: AuthorAssociation; +} + +export type AuthorAssociation = + | 'COLLABORATOR' + | 'CONTRIBUTOR' + | 'FIRST_TIMER' + | 'FIRST_TIME_CONTRIBUTOR' + | 'MANNEQUIN' + | 'MEMBER' + | 'NONE' + | 'OWNER'; diff --git a/integrations/github-issues/src/github-api/index.ts b/integrations/github-issues/src/github-api/index.ts new file mode 100644 index 000000000..ef6df7c39 --- /dev/null +++ b/integrations/github-issues/src/github-api/index.ts @@ -0,0 +1,3 @@ +export * from './client'; +export * from './rest'; +export * from './graphql'; diff --git a/integrations/github-issues/src/github-api/rest.ts b/integrations/github-issues/src/github-api/rest.ts new file mode 100644 index 000000000..96206e746 --- /dev/null +++ b/integrations/github-issues/src/github-api/rest.ts @@ -0,0 +1,30 @@ +import type { Octokit } from 'octokit'; + +import { Logger } from '@gitbook/runtime'; +import { GitHubIssuesRepository } from '../types'; + +const logger = Logger('github-issues:github-client'); + +/** + * Fetch repositories with issues from a specific GitHub App installation + */ +export async function getGitHubReposForInstallation( + octokit: Octokit, + installationId: string, +): Promise { + try { + const response = await octokit.request('GET /installation/repositories', { + per_page: 100, + }); + + const repositories = response.data.repositories; + + return repositories.filter((repo) => repo.has_issues); + } catch (error) { + logger.error( + `Failed to fetch GitHub repositories with issue for installation ${installationId}: `, + error instanceof Error ? error.message : String(error), + ); + return []; + } +} diff --git a/integrations/github-issues/src/index.ts b/integrations/github-issues/src/index.ts new file mode 100644 index 000000000..d3c24825b --- /dev/null +++ b/integrations/github-issues/src/index.ts @@ -0,0 +1,148 @@ +import { createIntegration, Logger } from '@gitbook/runtime'; +import { Router } from 'itty-router'; +import type { + GitHubIssuesRuntimeContext, + GitHubWebhookEventPayload, + GitHubWebhookEventType, +} from './types'; +import { configComponent } from './components'; +import { handleGitHubAppSetup } from './setup'; +import { + handleGitHubAppInstallationDeletedEvent, + handleGitHubAppRepositoryAddedToInstallation, + handlerGitHubIssueClosed, + verifyGitHubWebhookSignature, +} from './webhook'; +import { triggerInitialGitHubIssuesIngestion } from './ingestion'; +import { getGitHubInstallationIds } from './utils'; +import { handleGitHubIssuesIntegrationTask } from './tasks'; + +const logger = Logger('github-issues'); + +export default createIntegration({ + fetch: async (request, context) => { + const router = Router({ + base: new URL( + context.environment.installation?.urls.publicEndpoint || + context.environment.integration.urls.publicEndpoint, + ).pathname, + }); + + /* + * GitHub webhook events handlers. + */ + router.post('/webhook', async (request: Request) => { + const rawBody = await request.text(); + const githubEvent = request.headers.get('x-github-event') as GitHubWebhookEventType; + + logger.info(`received GitHub "${githubEvent}" webhook event`); + + const isSignatureValid = await verifyGitHubWebhookSignature(context, request, rawBody); + if (!isSignatureValid) { + return new Response('Unauthorized', { status: 401 }); + } + + if (!rawBody) { + return new Response('Malformed webhook', { status: 412 }); + } + + switch (githubEvent) { + case 'installation': { + const eventPayload: GitHubWebhookEventPayload['installation'] = + JSON.parse(rawBody); + + if (eventPayload.action !== 'deleted') { + // We handle created installation when the GitHub installation setup callback is called + // in order to properly linked them to an GitBook integration installation ID. + + // Other actions are not supported yet. + break; + } + + return await handleGitHubAppInstallationDeletedEvent(context, eventPayload); + } + case 'installation_repositories': { + const eventPayload: GitHubWebhookEventPayload['installation_repositories'] = + JSON.parse(rawBody); + + if (eventPayload.action !== 'added') { + break; + } + + return await handleGitHubAppRepositoryAddedToInstallation( + context, + eventPayload, + ); + } + case 'issues': { + const eventPayload: GitHubWebhookEventPayload['issues'] = JSON.parse(rawBody); + + if (eventPayload.action !== 'closed') { + break; + } + + return await handlerGitHubIssueClosed(context, eventPayload); + } + } + + return new Response('OK', { status: 200 }); + }); + + /** + * Github app setup handler that the user is redirected to after installing the app. + */ + router.get('/setup', async () => { + const url = new URL(request.url); + const githubAppInstallationId = url.searchParams.get('installation_id'); + const unverifiedAppSetupState = url.searchParams.get('state'); + + if (!unverifiedAppSetupState || !githubAppInstallationId) { + // The installation has been initiated from GitHub directly without going through GitBook. + // In this case redirect the user back to GitBook so we can properly link to an GitBook installation. + return Response.redirect(context.environment.integration.urls.app); + } + + const setupResponse = await handleGitHubAppSetup({ + context, + githubAppInstallationId, + unverifiedAppSetupState, + }); + + return setupResponse; + }); + + const response = await router.handle(request, context); + if (!response) { + return new Response(`No route matching ${request.method} ${request.url}`, { + status: 404, + }); + } + + return response; + }, + components: [configComponent], + events: { + installation_setup: async (_, context) => { + const githubInstallationIds = getGitHubInstallationIds(context); + const hasInstallations = githubInstallationIds.length > 0; + const gitbookInstallationId = context.environment.installation?.id; + + if (!hasInstallations) { + logger.info( + `GitBook installation ${gitbookInstallationId} has no associated GitHub installations. Skipping initial ingestion.`, + ); + return; + } + + try { + await triggerInitialGitHubIssuesIngestion(context); + } catch (error) { + logger.error( + `GitBook installation ${gitbookInstallationId} setup failed: `, + error instanceof Error ? error.message : String(error), + ); + } + }, + }, + task: handleGitHubIssuesIntegrationTask, +}); diff --git a/integrations/github-issues/src/ingestion.ts b/integrations/github-issues/src/ingestion.ts new file mode 100644 index 000000000..d2fcc82f0 --- /dev/null +++ b/integrations/github-issues/src/ingestion.ts @@ -0,0 +1,173 @@ +import pMap from 'p-map'; + +import { ExposableError, Logger } from '@gitbook/runtime'; +import { GitHubIssuesRuntimeContext } from './types'; +import { getGitHubInstallationIds } from './utils'; +import { + AuthorAssociation, + getGitHubReposForInstallation, + getOctokitClientForInstallation, + GitHubIssue, +} from './github-api'; +import { + ConversationInput, + ConversationPartMessage, + IntegrationInstallation, + Organization, +} from '@gitbook/api'; + +const logger = Logger('github-issues:ingest'); + +/** + * Trigger an initial ingestion of issues from all of the linked GitHub respositories + * of a GitBook installation. + */ +export async function triggerInitialGitHubIssuesIngestion(context: GitHubIssuesRuntimeContext) { + const { installation: gitbookInstallation } = context.environment; + if (!gitbookInstallation) { + throw new ExposableError('GitBook installation not found'); + } + + const githubInstallationIds = getGitHubInstallationIds(context); + if (githubInstallationIds.length === 0) { + throw new ExposableError('No GitHub App installation IDs found'); + } + + let totalRepoToProcess = 0; + const pendingTaskPromises: Array> = []; + + await pMap( + githubInstallationIds, + async (githubInstallationId) => { + try { + const octokit = await getOctokitClientForInstallation( + context, + githubInstallationId, + ); + const repos = await getGitHubReposForInstallation(octokit, githubInstallationId); + + if (repos.length === 0) { + logger.info( + `No GitHub repositories found for installation ${githubInstallationId}. Skipping.`, + ); + return; + } + + totalRepoToProcess += repos.length; + for (const repo of repos) { + pendingTaskPromises.push( + context.integration.queueTask({ + task: { + type: 'ingest:github-repo:closed-issues', + payload: { + organization: gitbookInstallation.target.organization, + gitbookInstallationId: gitbookInstallation.id, + githubInstallationId: githubInstallationId, + repository: { + owner: repo.owner.login, + name: repo.name, + }, + }, + }, + }), + ); + } + } catch (err) { + logger.error( + `Error while fetching repositories for installation ${githubInstallationId}: `, + err, + ); + return; + } + }, + { concurrency: 5 }, + ); + + context.waitUntil(Promise.all(pendingTaskPromises)); + + logger.info( + `Dispatched ${pendingTaskPromises.length} tasks to ingest a total of ${totalRepoToProcess} github repositories`, + ); +} + +/** + * Ingest a single GitHub issue as a GitBook conversation. + */ +export async function ingestGitHubIssue(args: { + organizationId: Organization['id']; + gitbookInstallationId: IntegrationInstallation['id']; + issue: GitHubIssue; + context: GitHubIssuesRuntimeContext; +}) { + const { organizationId, gitbookInstallationId, issue, context } = args; + const gitbookConversation = parseGitHubIssueAsGitBookConversation(issue); + + if (!gitbookConversation) { + logger.info(`Discarded GitHub issues as irrelevant: ${issue.url}`); + return; + } + + const installationApiClient = await context.api.createInstallationClient( + context.environment.integration.name, + gitbookInstallationId, + ); + await installationApiClient.orgs.ingestConversation(organizationId, [gitbookConversation]); +} + +/** + * Parse a GitHub issue into GitBook conversation input to be ingested. + */ +function parseGitHubIssueAsGitBookConversation(issue: GitHubIssue): ConversationInput | null { + // Filter out issues with empty content + if (issue.body.length === 0 && issue.comments.nodes.length == 0) { + return null; + } + + const conversation: ConversationInput = { + id: issue.id, + subject: issue.title, + metadata: { + url: issue.url, + attributes: { + repository: `${issue.repository.owner.login}/${issue.repository.name}`, + issue_number: String(issue.number), + }, + createdAt: issue.createdAt, + }, + parts: [ + { + type: 'message', + role: determineMessageRole(issue.authorAssociation), + body: issue.body, + }, + ], + }; + + for (const comment of issue.comments.nodes) { + conversation.parts.push({ + type: 'message', + role: determineMessageRole(comment.authorAssociation), + body: comment.body, + }); + } + + return conversation; +} + +/** + * Determine the role of a comment author in the conversation. + * Uses GitHub's authorAssociation to classify comments as user or team-member. + */ +function determineMessageRole( + authorAssociation: AuthorAssociation, +): ConversationPartMessage['role'] { + switch (authorAssociation) { + case 'OWNER': + case 'MEMBER': + case 'COLLABORATOR': + return 'team-member'; + + default: + return 'user'; + } +} diff --git a/integrations/github-issues/src/setup.ts b/integrations/github-issues/src/setup.ts new file mode 100644 index 000000000..cae0f53b0 --- /dev/null +++ b/integrations/github-issues/src/setup.ts @@ -0,0 +1,132 @@ +import jwt from '@tsndr/cloudflare-worker-jwt'; + +import { ExposableError, Logger } from '@gitbook/runtime'; +import { GitHubIssuesRuntimeContext } from './types'; + +const logger = Logger('github-issues:app-setup'); + +/** + * Handle GitHub app installattion setup requests. + */ +export async function handleGitHubAppSetup(args: { + context: GitHubIssuesRuntimeContext; + githubAppInstallationId: string; + unverifiedAppSetupState: string; +}) { + const { context, githubAppInstallationId, unverifiedAppSetupState } = args; + + const { gitbookInstallationId } = await verifyGitHubAppSetupState( + context, + unverifiedAppSetupState, + ); + + if (!gitbookInstallationId) { + return new ExposableError('Missing GitBook installation ID in GitHub app setup state'); + } + + try { + const { data: installation } = + await context.api.integrations.getIntegrationInstallationById( + context.environment.integration.name, + gitbookInstallationId, + ); + + const existingConfig = installation?.configuration || {}; + const existingInstallationIds = existingConfig.installation_ids || []; + + const updatedInstallationIds = Array.from( + new Set([...existingInstallationIds, githubAppInstallationId]), + ); + + await context.api.integrations.updateIntegrationInstallation( + context.environment.integration.name, + gitbookInstallationId, + { + configuration: { + ...existingConfig, + installation_ids: updatedInstallationIds, + }, + externalIds: updatedInstallationIds, + }, + ); + + return new Response( + ` + +

GitHub App Connected!

+

Your GitHub App has been successfully connected to GitBook.

+

We'll start ingesting your GitHub issues shortly.

+ + + `, + { + headers: { + 'Content-Type': 'text/html', + }, + }, + ); + } catch (error) { + logger.error( + `Failed to update GitBook installation ${gitbookInstallationId} with GitHub installation ${githubAppInstallationId}: `, + error instanceof Error ? error.message : String(error), + ); + + return new Response( + ` + +

Setup Failed

+

There was an error connecting your GitHub App to GitBook.

+

Please try again, or contact support and providing them with this error:

+
Error: ${error instanceof Error ? error.message : String(error)}
+ + `, + { + status: 500, + headers: { 'Content-Type': 'text/html' }, + }, + ); + } +} + +interface GitHubAppSetupState { + /** + * The Gitbook integration installation ID the GitHub app setup is linked to. + */ + gitbookInstallationId: string; +} + +/** + * Create a JWT signed with integration secret to store a GitHub app setup state. + */ +export async function createGitHubAppSetupState( + context: GitHubIssuesRuntimeContext, + state: GitHubAppSetupState, +) { + const token = await jwt.sign( + { gitbookInstallationId: state.gitbookInstallationId }, + context.environment.signingSecrets.integration, + ); + return token; +} + +/** + * Verify the signature of a JWT token that was passed as state of a Github app post installation. + */ +export async function verifyGitHubAppSetupState( + context: GitHubIssuesRuntimeContext, + token: string, +) { + const verifiedToken = await jwt.verify( + token, + context.environment.signingSecrets.integration, + ); + if (!verifiedToken) { + throw new ExposableError('Invalid GitHub app setup state token signature'); + } + + const { payload } = verifiedToken; + if (!payload || typeof payload.gitbookInstallationId !== 'string') { + throw new ExposableError('Malformed GitHub app setup state token'); + } + return payload; +} diff --git a/integrations/github-issues/src/tasks.ts b/integrations/github-issues/src/tasks.ts new file mode 100644 index 000000000..f842236f2 --- /dev/null +++ b/integrations/github-issues/src/tasks.ts @@ -0,0 +1,137 @@ +import { Logger } from '@gitbook/runtime'; +import { + getGitHubRepoClosedIssueIdsLast30Days, + getGitHubRepoIssuesByIds, + getOctokitClientForInstallation, +} from './github-api'; +import { + GitHubIssuesIntegrationIngestRepoClosedIssues, + GitHubIssuesIntegrationIngestRepoIssuesBatch, + GitHubIssuesIntegrationTask, + GitHubIssuesRuntimeContext, +} from './types'; +import pMap from 'p-map'; +import { ingestGitHubIssue } from './ingestion'; + +const logger = Logger('github-issues:tasks'); + +/** + * Handle an Github integration dispatched task. + */ +export async function handleGitHubIssuesIntegrationTask( + task: GitHubIssuesIntegrationTask, + context: GitHubIssuesRuntimeContext, +): Promise { + const { type: taskType } = task; + switch (taskType) { + case 'ingest:github-repo:closed-issues': { + await handleIngestGitHubRepoClosedIssuesTask(task, context); + break; + } + case 'ingest:github-repo:issues-batch': { + await handleIngestGitHubRepoIssuesBatch(task, context); + break; + } + default: + throw new Error(`Unknown github-issues integration task type: ${task}`); + } +} + +/** + * Handle the ingestion of all closed issues of a GitHub repository. + */ +async function handleIngestGitHubRepoClosedIssuesTask( + task: GitHubIssuesIntegrationIngestRepoClosedIssues, + context: GitHubIssuesRuntimeContext, +) { + const { payload } = task; + const octokit = await getOctokitClientForInstallation(context, payload.githubInstallationId); + + let after: string | null = null; + const limit = 50; + + let hasNextPage = true; + + const pendingTasksPromises: Array> = []; + + let totalIssuesToIngest = 0; + while (hasNextPage) { + const response = await getGitHubRepoClosedIssueIdsLast30Days({ + octokit, + repository: payload.repository, + page: { after, limit }, + }); + + if (!response.search) { + break; + } + + pendingTasksPromises.push( + context.integration.queueTask({ + task: { + type: 'ingest:github-repo:issues-batch', + payload: { + gitbookInstallationId: payload.gitbookInstallationId, + githubInstallationId: payload.githubInstallationId, + organization: payload.organization, + repository: payload.repository, + issuesIds: response.search.nodes.map((issue) => issue.id), + }, + }, + }), + ); + + totalIssuesToIngest += response.search.nodes.length; + + hasNextPage = response.search.pageInfo.hasNextPage; + after = response.search.pageInfo.endCursor ?? null; + } + + context.waitUntil(Promise.all(pendingTasksPromises)); + + logger.info( + `Dispatched ${totalIssuesToIngest} issues to ingest from ${payload.repository.owner}/${payload.repository.name} (GitBook installation ${payload.gitbookInstallationId})`, + ); +} + +/** + * Handle the ingestion of a batch of issues from a GitHub repository. + */ +async function handleIngestGitHubRepoIssuesBatch( + task: GitHubIssuesIntegrationIngestRepoIssuesBatch, + context: GitHubIssuesRuntimeContext, +) { + const { payload } = task; + const octokit = await getOctokitClientForInstallation(context, payload.githubInstallationId); + + const response = await getGitHubRepoIssuesByIds({ octokit, issueIds: payload.issuesIds }); + + if (!response.nodes || response.nodes.length === 0) { + logger.info( + `No GitHub issues found with IDs: ${JSON.stringify(payload.issuesIds)} for GitBook installation ${payload.gitbookInstallationId}`, + ); + return; + } + + const issues = response.nodes; + let totalIngested = 0; + + await pMap( + issues, + async (issue) => { + await ingestGitHubIssue({ + organizationId: payload.organization, + gitbookInstallationId: payload.gitbookInstallationId, + issue, + context, + }); + + totalIngested += 1; + }, + { concurrency: 5 }, + ); + + logger.info( + `Ingested ${totalIngested} issues from ${payload.repository.owner}/${payload.repository.name} (GitBook installation ${payload.gitbookInstallationId})`, + ); +} diff --git a/integrations/github-issues/src/types.ts b/integrations/github-issues/src/types.ts new file mode 100644 index 000000000..534a33ad9 --- /dev/null +++ b/integrations/github-issues/src/types.ts @@ -0,0 +1,80 @@ +import { IntegrationInstallation, Organization } from '@gitbook/api'; +import { RuntimeEnvironment, RuntimeContext } from '@gitbook/runtime'; +import type { components } from '@octokit/openapi-types'; +import type { EventPayloadMap } from '@octokit/webhooks-types'; + +export type GitHubIssuesInstallationConfiguration = { + /** + * The GitHub app installation IDs associated with the GitBook integration installation. + */ + installation_ids?: string[]; +}; + +export type GitHubIssuesRuntimeEnvironment = + RuntimeEnvironment; +export type GitHubIssuesRuntimeContext = RuntimeContext< + GitHubIssuesRuntimeEnvironment, + GitHubIssuesIntegrationTask +>; + +/** + * Integration tasks. + */ +type GitHubIssuesIntegrationTaskType = + | 'ingest:github-repo:closed-issues' + | 'ingest:github-repo:issues-batch'; + +type GitHubIssuesIntegrationBaseTask< + Type extends GitHubIssuesIntegrationTaskType, + Payload extends object, +> = { + type: Type; + payload: Payload; +}; + +/** + * Task dispatched to ingest all recently closed issues from a GitHub repository. + */ +export type GitHubIssuesIntegrationIngestRepoClosedIssues = GitHubIssuesIntegrationBaseTask< + 'ingest:github-repo:closed-issues', + { + organization: Organization['id']; + gitbookInstallationId: IntegrationInstallation['id']; + githubInstallationId: string; + repository: { + name: GitHubIssuesRepository['name']; + owner: GitHubIssuesRepository['owner']['login']; + }; + } +>; + +/** + * Task dispatched to ingest a batch of GitHub issue from a repo. + */ +export type GitHubIssuesIntegrationIngestRepoIssuesBatch = GitHubIssuesIntegrationBaseTask< + 'ingest:github-repo:issues-batch', + { + organization: Organization['id']; + gitbookInstallationId: IntegrationInstallation['id']; + githubInstallationId: string; + issuesIds: string[]; + repository: { + name: GitHubIssuesRepository['name']; + owner: GitHubIssuesRepository['owner']['login']; + }; + } +>; + +export type GitHubIssuesIntegrationTask = + | GitHubIssuesIntegrationIngestRepoClosedIssues + | GitHubIssuesIntegrationIngestRepoIssuesBatch; + +/** + * GitHub API/webhook schemas types. + */ +export type GitHubIssuesAppInstallation = components['schemas']['installation']; +export type GitHubIssuesIssue = components['schemas']['issue']; +export type GitHubIssuesRepository = components['schemas']['repository']; + +export type GitHubWebhookEventType = keyof EventPayloadMap; +export type GitHubWebhookEventPayload = EventPayloadMap; diff --git a/integrations/github-issues/src/utils.ts b/integrations/github-issues/src/utils.ts new file mode 100644 index 000000000..1497353bd --- /dev/null +++ b/integrations/github-issues/src/utils.ts @@ -0,0 +1,13 @@ +import { GitHubIssuesRuntimeContext } from './types'; + +/** + * Get all GitHub installation IDs associated with a GitBook integration installation. + */ +export function getGitHubInstallationIds(context: GitHubIssuesRuntimeContext): string[] { + const { installation } = context.environment; + if (!installation) { + return []; + } + + return installation.configuration.installation_ids || []; +} diff --git a/integrations/github-issues/src/webhook.ts b/integrations/github-issues/src/webhook.ts new file mode 100644 index 000000000..924bb9f3d --- /dev/null +++ b/integrations/github-issues/src/webhook.ts @@ -0,0 +1,263 @@ +import pMap from 'p-map'; + +import { ExposableError, Logger } from '@gitbook/runtime'; +import { GitHubIssuesRuntimeContext, GitHubWebhookEventPayload } from './types'; +import { getGitHubInstallationIds } from './utils'; +import { ingestGitHubIssue } from './ingestion'; +import { getGitHubRepoIssueById, getOctokitClientForInstallation } from './github-api'; + +const logger = Logger('github-issues:webhook'); + +/** + * Process a GitHub App "installation" deleted event by removing it from the linked GitBook installations. + */ +export async function handleGitHubAppInstallationDeletedEvent( + context: GitHubIssuesRuntimeContext, + payload: Extract, +) { + const githubInstallationId = String(payload.installation.id); + + logger.info( + `handling GitHub App installation deleted event for installation ID ${githubInstallationId}...`, + ); + + const { + data: { items: installations }, + } = await context.api.integrations.listIntegrationInstallations( + context.environment.integration.name, + { + externalId: githubInstallationId, + }, + ); + + if (installations.length === 0) { + logger.info( + `No GitBook installations found for deleted GitHub installation: ${githubInstallationId}`, + ); + return new Response('Installation webhook received', { status: 200 }); + } + + await pMap( + installations, + async (installation) => { + try { + const existingConfig = installation.configuration || {}; + const existingInstallationIds = existingConfig.installation_ids || []; + + const updatedInstallationIds = existingInstallationIds.filter( + (id: string) => id !== githubInstallationId, + ); + + await context.api.integrations.updateIntegrationInstallation( + context.environment.integration.name, + installation.id, + { + configuration: { + ...existingConfig, + installation_ids: updatedInstallationIds, + }, + externalIds: updatedInstallationIds, + }, + ); + + logger.info( + `GitHub App installation ${githubInstallationId} removed from GitBook installation ${installation.id}`, + ); + } catch (error) { + logger.error( + `Error removing GitHub installation ${githubInstallationId} from GitBook installation ${installation.id}: `, + error instanceof Error ? error.message : String(error), + ); + } + }, + { concurrency: 5 }, + ); + + return new Response('Installation webhook received', { status: 200 }); +} + +/** + * Process a GitHub App "installation_repositories" event when an installation was granted access to one or more repositories + * by triggering an inital ingestion for these repos. + */ +export async function handleGitHubAppRepositoryAddedToInstallation( + context: GitHubIssuesRuntimeContext, + payload: Extract, +) { + const githubInstallationId = String(payload.installation.id); + + logger.info( + `handling GitHub App installation_repositories event with added repositories for installation ID ${githubInstallationId}...`, + ); + + const { + data: { items: installations }, + } = await context.api.integrations.listIntegrationInstallations( + context.environment.integration.name, + { + externalId: githubInstallationId, + }, + ); + + if (installations.length === 0) { + logger.info( + `No GitBook installations found for GitHub installation: ${githubInstallationId}`, + ); + return new Response('Installation webhook received', { status: 200 }); + } + + const addedRepos = payload.repositories_added.map((addedRepo) => { + const [owner, name] = addedRepo.full_name.split('/'); + + return { owner, name }; + }); + + let totalRepoToProcess = addedRepos.length; + + if (!totalRepoToProcess) { + logger.info('No repository added. Skipping'); + } + + const pendingTaskPromises: Array> = []; + + for (const gitbookInstallation of installations) { + for (const repo of addedRepos) { + pendingTaskPromises.push( + context.integration.queueTask({ + task: { + type: 'ingest:github-repo:closed-issues', + payload: { + organization: gitbookInstallation.target.organization, + gitbookInstallationId: gitbookInstallation.id, + githubInstallationId: githubInstallationId, + repository: { + owner: repo.owner, + name: repo.name, + }, + }, + }, + }), + ); + } + } + + context.waitUntil(Promise.all(pendingTaskPromises)); + + logger.info( + `Dispatched ${pendingTaskPromises.length} tasks to ingest a total of ${totalRepoToProcess} github repositories`, + ); + + return new Response('Installation repositories added webhook received', { status: 200 }); +} + +/** + * Process a GitHub issue closed event by ingesting the details of the issue. + */ +export async function handlerGitHubIssueClosed( + context: GitHubIssuesRuntimeContext, + payload: Extract, +) { + const { issue, installation: githubInstallation } = payload; + + if (!githubInstallation) { + throw new ExposableError('Missing GitHub installation ID from event'); + } + + const githubInstallationId = String(githubInstallation.id); + + logger.info(`handling GitHub for installation ID ${githubInstallationId}...`); + + const { + data: { items: installations }, + } = await context.api.integrations.listIntegrationInstallations( + context.environment.integration.name, + { + externalId: githubInstallationId, + }, + ); + + if (installations.length === 0) { + logger.info( + `No GitBook installations found for GitHub installation: ${githubInstallationId}`, + ); + return new Response('Issue closed webhook received', { status: 200 }); + } + + const octokit = await getOctokitClientForInstallation(context, githubInstallationId); + const response = await getGitHubRepoIssueById({ octokit, issueId: String(issue.node_id) }); + + if (!response.node) { + logger.info( + `No GitHub issue found with ID: ${issue.node_id} for GitHub installation ${githubInstallation.id}`, + ); + return; + } + + await pMap( + installations, + async (gitbookInstallation) => { + try { + await ingestGitHubIssue({ + organizationId: gitbookInstallation.target.organization, + gitbookInstallationId: gitbookInstallation.id, + issue: response.node, + context, + }); + + logger.info( + `Triggered ingestion of GitHub issue ${response.node.url} for GitBook installation ${gitbookInstallation.id}`, + ); + } catch (error) { + logger.error( + `Error triggering ingestion of GitHub issue ${response.node.url} for GitBook installation ${gitbookInstallation.id}: `, + error instanceof Error ? error.message : String(error), + ); + } + }, + { concurrency: 5 }, + ); + + return new Response('Issue closed webhook received', { status: 200 }); +} + +/** + * Verify a GitHub webhook signature. + * + * {@link https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries} + */ +export async function verifyGitHubWebhookSignature( + context: GitHubIssuesRuntimeContext, + request: Request, + rawBody: string, +): Promise { + const signature = request.headers.get('x-hub-signature-256'); + const webhookSecret = context.environment.secrets.WEBHOOK_SECRET; + + if (!signature || !webhookSecret) { + return false; + } + + try { + const encoder = new TextEncoder(); + const key = await crypto.subtle.importKey( + 'raw', + encoder.encode(webhookSecret), + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign'], + ); + + const signatureBuffer = await crypto.subtle.sign('HMAC', key, encoder.encode(rawBody)); + const hashArray = Array.from(new Uint8Array(signatureBuffer)); + const computedSignature = + 'sha256=' + hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); + + return computedSignature === signature; + } catch (error) { + logger.error( + 'Failed to verify webhook signature: ', + error instanceof Error ? error.message : String(error), + ); + return false; + } +} diff --git a/integrations/github-issues/tsconfig.json b/integrations/github-issues/tsconfig.json new file mode 100644 index 000000000..1a48f875b --- /dev/null +++ b/integrations/github-issues/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "@gitbook/tsconfig/integration.json" +} diff --git a/turbo.json b/turbo.json index 437ed3754..287f6f1e3 100644 --- a/turbo.json +++ b/turbo.json @@ -33,6 +33,7 @@ "GITHUB_CLIENT_ID", "GITHUB_CLIENT_SECRET", "GITHUB_WEBHOOK_SECRET", + "GITHUB_APP_NAME", "GITHUB_PRIVATE_KEY", "LUCID_CLIENT_ID", "LUCID_CLIENT_SECRET",