Skip to content

Commit d7914ca

Browse files
feat(task-manager): GitHub integration (#617) (#619)
* Add GitHub integration service and tests Introduces a GitHubService class for interacting with the GitHub API, including methods for app installation, issue creation, and Copilot assignment. Adds related environment variables to .env.sample and type definitions to env.d.ts. Updates dependencies to include @octokit/rest, @octokit/types, and jsonwebtoken v9.0.3. Provides comprehensive tests for the new integration. * Add Task Manager integration mutations and types Introduces GraphQL types and mutations for managing Task Manager integration in projects, including disconnecting and updating settings. Updates the project model to support partial updates, extends resolvers for new mutations, and adds comprehensive tests for these features. * Add GitHub integration with installation flow Introduces GitHub integration endpoints and service, including a Redis-backed state store for secure installation flow. Refactors integration code into modular files, appends GitHub routes to the Express app, and updates tests to import the service from its new location. * Refactor GitHub integration connect endpoint and update dependencies Refactored the /integration/github/connect endpoint to return a JSON object with the installation redirect URL instead of performing a direct redirect, and added colorized logging with environment-based suppression for tests. Updated Jest and argon2 dependencies, added Jest moduleNameMapper for node:crypto and node:util, and introduced mocks for these modules. Added comprehensive tests for the GitHub integration connect route. * Add GitHub integration callback and webhook endpoints Implemented /callback endpoint to handle GitHub App installation callbacks and save configuration to the project. Added /webhook endpoint to securely process GitHub webhook events, including removal of taskManager config on installation deletion. Improved logging with project context and added utility functions for URL building and signature verification. * Add GitHub repository selection endpoints and types Introduces endpoints to list and update GitHub repositories for a project, adds a Repository type to the GitHub service, and refactors project admin access validation. Also adds the TaskManagerItem GraphQL type and links it to the Event type for improved integration with task managers like GitHub Issues. * Add GitHub OAuth integration and token management Introduces GitHub OAuth flow for user-to-server tokens, including endpoints for handling OAuth callbacks and exchanging codes for tokens. Updates the GitHub service to support OAuth code exchange, token validation, and refresh, and adds required environment variables for client ID and secret. Updates dependencies to support new OAuth methods and expands the project model to store task manager configuration. * Refactor Copilot assignment to use GraphQL and OAuth Updated the assignCopilot method to use the user-to-server OAuth token and GitHub GraphQL API for assigning the Copilot agent to issues. Improved error handling, added detailed logging, and ensured compatibility with the Copilot bot assignment process. Also added input validation for installationId in relevant methods. * Bump version up to 1.3.2 * Update package.json * lint code * fix tests * fix integration tests due to jest 30 update * Improve GitHub integration error handling and tests Refactored GitHub OAuth and installation flow to provide more accurate error redirects and preserve existing taskManager config values. Enhanced timing-safe signature validation using crypto.timingSafeEqual. Updated GraphQL query in GitHubService to use variable for issue number. Expanded and improved test coverage for GitHub routes, including edge cases and config preservation. Refactored project resolver tests for better type safety and error handling. * Preserve null in project update for taskManager Adjusts mock project update logic to correctly preserve null values for taskManager when explicitly set, ensuring accurate test behavior for scenarios like disconnectTaskManager. Also refactors test setup in GitHub routes integration test for clarity. --------- Co-authored-by: Peter <specc.dev@gmail.com> Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 2f3b720 commit d7914ca

34 files changed

+6246
-2402
lines changed

.env.sample

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,13 @@ GITHUB_CLIENT_ID=fakedata
5151
GITHUB_CLIENT_SECRET=fakedata
5252

5353
## Hawk API public url (used in OAuth to redirect to callback, should match OAuth app callback URL)
54-
API_URL=http://127.0.0.1:4000
54+
API_URL=http://localhost:4000
5555

5656
## Garage url
57-
GARAGE_URL=http://127.0.0.1:8080
57+
GARAGE_URL=http://localhost:8080
5858

5959
## Garage login url
60-
GARAGE_LOGIN_URL=http://127.0.0.1:8080/login
60+
GARAGE_LOGIN_URL=http://localhost:8080/login
6161

6262
## Hawk Catcher token from hawk.so
6363
HAWK_CATCHER_TOKEN=
@@ -93,3 +93,47 @@ SSO_SP_ENTITY_ID=urn:hawk:tracker:saml
9393

9494
## SAML state store type (memory or redis, default: redis)
9595
SAML_STORE_TYPE=redis
96+
97+
# String generated when we create GitHub App, see task-managers-integration-implementation-plan.md -> 2.1.1
98+
GITHUB_WEBHOOK_SECRET=623f6ed30b1f762803149893263a95cc2687fe3ce5a9f30648dcbf25712afc9e
99+
100+
# Id of GitHub app
101+
GITHUB_APP_ID=1234567
102+
103+
# Client ID of GitHub app
104+
GITHUB_APP_CLIENT_ID=Iv23li65HEIkWZXsm6qO
105+
106+
# GitHub App slug/name. Used to generate installation URLs
107+
GITHUB_APP_SLUG=hawk-tracker-app
108+
109+
# Private key generated in GitHub app settings
110+
GITHUB_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
111+
MIIEpAIBAAKCAQEA0r16047BqAxFgbltKcNOt9RGMZ2COI2ui7Ujmn9vtMV83HHu
112+
lN/ek4kLTz8nunUc0s21xWpW7mjoaO61qSzcEn7vDhgMOAnq+wq+iJsk5IM/MJNe
113+
sNatymMVO6Q6UuVx4Nshac3T7M8dVWx3Oc9ef9nc/dXdXTQn73EZC5pKqKoFE+yv
114+
NGHS0JAgUGa1zDt/TqXcypz06tyrFgZFFuBo01kS1xUU/J4bpwNnk9KNq7lakdAE
115+
JeJ/BzwTRZUzSIjtLSDjFbcjI2iXsTOasrygXDTYYcnvQf4HjRxBuCwIuGo4mOHz
116+
86l5Icq9dNWq1Nj+FJW9IMEOYW0927uws+dotwIDAQABAoIBAHfdRitmm0eWErEm
117+
YqzKZc+xcWtvB05bZ9gW43VQ3pyXZ3mLZARRgSuxWzlr1pD7Y7WTQ7xRy7g2+1oT
118+
zEe5OENc52PA0dJd8cVwSwcwFz/SVvKuH8G9mYPv73fI5VOZJbibatnfNJcRBsI7
119+
u2SqSjm2FThbmFkW/U/3qCMtUyGy6atoUZvoeQ5aUKmW7Z4gaPVN3z+265qJwR3o
120+
qnNUY2FNEA8tsJ05EzqBp8+SYghAiKY1QKqSb2pIfbBNmx5ItB2WeH3sTaE3Zdzy
121+
Kk/5fDY/rlOouRg7kHQd9sPnXzy7LVhUaQG0fZdjBOpJFXHFqi1Gsf3HAMRHkhyJ
122+
+EvgOuECgYEA/B85RCgVagwm9otXyj5JGIbj2iSAEwKnk2fxiZsGC3pirK85ZITp
123+
bZiIvwMzCKiHlL0vjiVsFBL8tr7p5bP1g4GNTOCg2Dj+B/Or9IgxHxpDCIPrV+L0
124+
GBgJEsNylJESQU/xf6qBs88FLFLyRtwxXkucsfk+OrF5IBUaEdEuo9UCgYEA1ftO
125+
wiD4O3LTs5dZiUHBrLWGejfbltHILF4oA61O7pLlMWkBvN1H8pJOI9FbTHWlv4Vc
126+
UFApUjm4wUGArCKu7AfcSZ2+xhySIC3ilReXbQp7WqdKp/T/RaKh38zEa6MVMqAJ
127+
cYYoWj4/NJMN1a4+G/zU5adMVb0jITyJB1EYfFsCgYEApUSSdWsRHoL4x4Rv99L8
128+
d2d01Po4Oj3zO11Xp6xHOh7vr+Ls7Edz/LOQcCXYvkQ7G/UnxzYgssf/gIuFJ13g
129+
AmRaC5rz1MkHPI8umQztp3XAy0QucV4ERAb9a59S7LBsFwQgel96xjNeYL++sVSF
130+
yBoojUGk2TSdAbrTa/qDaEECgYBCrqD5gBq7M+pjEew2CMbZEmyI07Vbh55QrTrd
131+
AnoRgLdpsWZ4O6D7J7qwEMLZzePMDjwZTxHBbPl1R/tYKSrHpR9x1XWo+ShUXNg6
132+
S/LFaTnNo0pxkrimM6ssOfyP6m9lqlenB/61OKartJPgHf9+60hRFNSF933mEp5F
133+
KHFv9wKBgQCXaVz5sgtcWgkQYSn4XTwSYKaeYww4hnIel30pASrLujQ9FubAvdjZ
134+
apxW9AHmx4aVRrmcIPq/BlMc6lGIgx2IwMBvJVpHVeUOUMAfNMRZV0XjY715xEyW
135+
U/uCfmCh8rfyQ75rthD4mGzNmHBpWrP/bD3c/vdj0wAxFXVyR5bG/Q==
136+
-----END RSA PRIVATE KEY-----"
137+
138+
# Generated in GitHub app settings
139+
GITHUB_APP_CLIENT_SECRET=0663e20d484234e17b0871c1f070581739c14e04

docker-compose.test.yml

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
version: "3.4"
21
services:
32
api:
43
build:
@@ -16,7 +15,6 @@ services:
1615
- mongodb
1716
- rabbitmq
1817
- keycloak
19-
# - accounting
2018
stdin_open: true
2119
tty: true
2220

@@ -65,7 +63,7 @@ services:
6563
retries: 5
6664

6765
keycloak:
68-
image: quay.io/keycloak/keycloak:23.0
66+
image: quay.io/keycloak/keycloak:22.0
6967
environment:
7068
- KEYCLOAK_ADMIN=admin
7169
- KEYCLOAK_ADMIN_PASSWORD=admin
@@ -74,18 +72,21 @@ services:
7472
- KC_HOSTNAME_STRICT_HTTPS=false
7573
- KC_HTTP_ENABLED=true
7674
- KC_HEALTH_ENABLED=true
75+
- JAVA_OPTS_APPEND=-Djava.io.tmpdir=/opt/keycloak/data/tmp
7776
ports:
7877
- 8180:8180
7978
command:
8079
- start-dev
8180
volumes:
8281
- keycloak-test-data:/opt/keycloak/data
83-
- ./test/integration/keycloak:/opt/keycloak/config
82+
tmpfs:
83+
- /tmp:size=128M
8484
healthcheck:
85-
test: ["CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/8180;echo -e 'GET /health/ready HTTP/1.1\r\nhost: http://localhost\r\nConnection: close\r\n\r\n' >&3;if [ $? -eq 0 ]; then echo 'Healthcheck Successful';exit 0;else echo 'Healthcheck Failed';exit 1;fi;"]
85+
test: ["CMD", "bash", "-c", "exec 3<>/dev/tcp/127.0.0.1/8180 && echo -e 'GET /health/ready HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n' >&3 && cat <&3 | grep -q '200 OK'"]
8686
interval: 10s
87-
timeout: 5s
88-
retries: 10
87+
timeout: 10s
88+
retries: 15
89+
start_period: 60s
8990

9091
# accounting:
9192
# image: codexteamuser/codex-accounting:prod

jest.config.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ module.exports = {
2929
}],
3030
},
3131

32+
/**
33+
* Map node: prefixed imports to mock files
34+
* Jest 27 supports node: prefix for ESM imports, but CommonJS require('node:crypto')
35+
* in modules like argon2.cjs still needs explicit mapping to mocks
36+
*/
37+
moduleNameMapper: {
38+
'^node:crypto$': '<rootDir>/test/__mocks__/node_crypto.js',
39+
'^node:util$': '<rootDir>/test/__mocks__/node_util.js',
40+
},
41+
3242
/**
3343
* Ignore folders
3444
*/

package.json

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "hawk.api",
3-
"version": "1.3.2",
3+
"version": "1.4.0",
44
"main": "index.ts",
55
"license": "BUSL-1.1",
66
"scripts": {
@@ -22,16 +22,16 @@
2222
"devDependencies": {
2323
"@shelf/jest-mongodb": "^6.0.2",
2424
"@swc/core": "^1.3.0",
25-
"@types/jest": "^26.0.8",
25+
"@types/jest": "^29.5.0",
2626
"@types/xml2js": "^0.4.14",
2727
"eslint": "^6.7.2",
2828
"eslint-config-codex": "1.2.4",
2929
"eslint-plugin-import": "^2.19.1",
30-
"jest": "^26.2.2",
30+
"jest": "^30.2.0",
3131
"mongodb-memory-server": "^6.6.1",
3232
"nodemon": "^2.0.2",
3333
"redis-mock": "^0.56.3",
34-
"ts-jest": "^26.1.4",
34+
"ts-jest": "^29.4.0",
3535
"ts-node": "^10.9.1",
3636
"typescript": "^4.7.4",
3737
"xml2js": "^0.6.2"
@@ -43,9 +43,12 @@
4343
"@graphql-tools/schema": "^8.5.1",
4444
"@graphql-tools/utils": "^8.9.0",
4545
"@hawk.so/nodejs": "^3.1.1",
46-
"@hawk.so/types": "^0.4.2",
46+
"@hawk.so/types": "^0.5.6",
4747
"@n1ru4l/json-patch-plus": "^0.2.0",
4848
"@node-saml/node-saml": "^5.0.1",
49+
"@octokit/oauth-methods": "^4.0.0",
50+
"@octokit/rest": "^22.0.1",
51+
"@octokit/types": "^16.0.0",
4952
"@types/amqp-connection-manager": "^2.0.4",
5053
"@types/debug": "^4.1.5",
5154
"@types/escape-html": "^1.0.0",
@@ -62,7 +65,7 @@
6265
"amqp-connection-manager": "^3.1.0",
6366
"amqplib": "^0.5.5",
6467
"apollo-server-express": "^3.10.0",
65-
"argon2": "^0.28.7",
68+
"argon2": "^0.44.0",
6669
"aws-sdk": "^2.1174.0",
6770
"axios": "^0.27.2",
6871
"body-parser": "^1.19.0",
@@ -76,7 +79,7 @@
7679
"graphql-scalars": "^1.17.0",
7780
"graphql-type-json": "^0.3.0",
7881
"graphql-upload": "^13",
79-
"jsonwebtoken": "^8.5.1",
82+
"jsonwebtoken": "^9.0.3",
8083
"lodash": "^4.17.15",
8184
"lodash.clonedeep": "^4.5.0",
8285
"lodash.mergewith": "^4.6.2",

src/index.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { requestLogger } from './utils/logger';
3131
import ReleasesFactory from './models/releasesFactory';
3232
import RedisHelper from './redisHelper';
3333
import { appendSsoRoutes } from './sso';
34+
import { appendGitHubRoutes } from './integrations/github';
3435

3536
/**
3637
* Option to enable playground
@@ -248,20 +249,26 @@ class HawkAPI {
248249
await redis.initialize();
249250

250251
/**
251-
* Setup shared factories for SSO routes
252-
* SSO endpoints don't require per-request DataLoaders isolation,
252+
* Setup shared factories for SSO and GitHub integration routes
253+
* These endpoints don't require per-request DataLoaders isolation,
253254
* so we can reuse the same factories instance
254255
* Created here to avoid duplication with createContext
255256
*/
256257
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
257-
const ssoDataLoaders = new DataLoaders(mongo.databases.hawk!);
258-
const ssoFactories = HawkAPI.setupFactories(ssoDataLoaders);
258+
const sharedDataLoaders = new DataLoaders(mongo.databases.hawk!);
259+
const sharedFactories = HawkAPI.setupFactories(sharedDataLoaders);
259260

260261
/**
261262
* Append SSO routes to Express app using shared factories
262263
* Note: This must be called after database connections are established
263264
*/
264-
appendSsoRoutes(this.app, ssoFactories);
265+
appendSsoRoutes(this.app, sharedFactories);
266+
267+
/**
268+
* Append GitHub integration routes to Express app using shared factories
269+
* Note: This must be called after database connections are established
270+
*/
271+
appendGitHubRoutes(this.app, sharedFactories);
265272

266273
await this.server.start();
267274
this.app.use(graphqlUploadExpress());

src/integrations/github/index.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import express from 'express';
2+
import { createGitHubRouter } from './routes';
3+
import { ContextFactories } from '../../types/graphql';
4+
5+
/**
6+
* Re-export types and service from service.ts for backward compatibility
7+
*/
8+
export { GitHubService, IssueData, GitHubIssue, Installation, Repository } from './service';
9+
10+
/**
11+
* Append GitHub routes to Express app
12+
*
13+
* @param app - Express application instance
14+
* @param factories - context factories for database access
15+
*/
16+
export function appendGitHubRoutes(app: express.Application, factories: ContextFactories): void {
17+
const githubRouter = createGitHubRouter(factories);
18+
19+
app.use('/integration/github', githubRouter);
20+
}

0 commit comments

Comments
 (0)