-
Notifications
You must be signed in to change notification settings - Fork 85
Add GitHub issues integration #1079
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
39ae02d
Add bootstraping code
spastorelli 994c3a2
Handle GitHub app installation flow
spastorelli b24f3c9
Handle GitHub installation deleted webhook events
spastorelli 571bc06
Start implementing ingestion logic
spastorelli 9d8f2e6
Handle initial issues ingestion on integration installation
spastorelli 9d8ba36
Fix comment and add concurrency for batch ingestion
spastorelli 3d8e2dd
Improve typing for GitHub webhook events
spastorelli 7bf3a8b
Fix dispatch logic installation deleted event
spastorelli 57d2532
Fix PKCS8 error when signing token
spastorelli 5a31d9a
Add handling of event when repos get added to installations
spastorelli f2b9fe7
Add handling of issue closed event
spastorelli ccf164d
Fix format
spastorelli 7a5ae75
Add env var to turbo.json
spastorelli 7e58d52
Merge branch 'main' into steeve/github-issues-integration
spastorelli fb6d26f
Add env var to workflows
spastorelli c6a7378
review
spastorelli ec335a6
Add staging/prod secrets after creating GitHub apps
spastorelli 57cc0d7
review: remove GITHUB_APP_NAME reference from workflow
spastorelli 6e6de3c
Add changeset
spastorelli 0eed488
Fix webhook error after further tests
spastorelli File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@gitbook/integration-github-issues': patch | ||
| --- | ||
|
|
||
| Add GitHub issues integration |
Large diffs are not rendered by default.
Oops, something went wrong.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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: | ||
spastorelli marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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" }} | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<GitHubIssuesRuntimeEnvironment>, | ||
| {}, | ||
| 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 ( | ||
| <configuration> | ||
| <input | ||
| label="GitHub App Installation" | ||
| hint="Authorize GitBook to access your GitHub issues in your repositories." | ||
| element={ | ||
| <button | ||
| style="secondary" | ||
| disabled={false} | ||
| label={hasInstallations ? 'Manage repositories' : 'Install GitHub App'} | ||
| onPress={{ | ||
| action: '@ui.url.open', | ||
| url: githubAppInstallURL.toString(), | ||
| }} | ||
| /> | ||
| } | ||
| /> | ||
| </configuration> | ||
| ); | ||
| }, | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<Octokit> { | ||
| 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<string> { | ||
| 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<string> { | ||
| 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, | ||
| }; | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.