mirror of
https://github.com/eliasstepanik/core.git
synced 2026-01-11 16:48:27 +00:00
Feat: Github integration
This commit is contained in:
parent
ddc0b0085c
commit
7a890d4e13
@ -4,6 +4,8 @@ import { requireUserId } from "~/services/session.server";
|
||||
import { logger } from "~/services/logger.service";
|
||||
import { prisma } from "~/db.server";
|
||||
import { triggerIntegrationWebhook } from "~/trigger/webhooks/integration-webhook-delivery";
|
||||
import { scheduler } from "~/trigger/integrations/scheduler";
|
||||
import { schedules } from "@trigger.dev/sdk";
|
||||
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
if (request.method !== "POST") {
|
||||
@ -28,6 +30,10 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
},
|
||||
});
|
||||
|
||||
const integrationAccountSettings = updatedAccount.settings as any;
|
||||
|
||||
await schedules.del(integrationAccountSettings.scheduleId);
|
||||
|
||||
await triggerIntegrationWebhook(
|
||||
integrationAccountId,
|
||||
userId,
|
||||
|
||||
2
integrations/github/.gitignore
vendored
Normal file
2
integrations/github/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
bin
|
||||
node_modules
|
||||
22
integrations/github/.prettierrc
Normal file
22
integrations/github/.prettierrc
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"arrowParens": "always",
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"bracketSpacing": true,
|
||||
"jsxBracketSameLine": false,
|
||||
"requirePragma": false,
|
||||
"proseWrap": "preserve",
|
||||
"singleQuote": true,
|
||||
"formatOnSave": true,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 100,
|
||||
"overrides": [
|
||||
{
|
||||
"files": ".prettierrc",
|
||||
"options": {
|
||||
"parser": "json"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
98
integrations/github/eslint.config.js
Normal file
98
integrations/github/eslint.config.js
Normal file
@ -0,0 +1,98 @@
|
||||
const eslint = require('@eslint/js');
|
||||
const tseslint = require('typescript-eslint');
|
||||
const reactPlugin = require('eslint-plugin-react');
|
||||
const jestPlugin = require('eslint-plugin-jest');
|
||||
const importPlugin = require('eslint-plugin-import');
|
||||
const prettierPlugin = require('eslint-plugin-prettier');
|
||||
const unusedImportsPlugin = require('eslint-plugin-unused-imports');
|
||||
const jsxA11yPlugin = require('eslint-plugin-jsx-a11y');
|
||||
|
||||
module.exports = [
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
{
|
||||
files: ['**/*.{js,jsx,ts,tsx}'],
|
||||
plugins: {
|
||||
react: reactPlugin,
|
||||
jest: jestPlugin,
|
||||
import: importPlugin,
|
||||
prettier: prettierPlugin,
|
||||
'unused-imports': unusedImportsPlugin,
|
||||
'jsx-a11y': jsxA11yPlugin,
|
||||
},
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module',
|
||||
parser: tseslint.parser,
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'jsx-a11y/label-has-associated-control': 'error',
|
||||
curly: 'warn',
|
||||
'dot-location': 'warn',
|
||||
eqeqeq: 'error',
|
||||
'prettier/prettier': 'warn',
|
||||
'unused-imports/no-unused-imports': 'warn',
|
||||
'no-else-return': 'warn',
|
||||
'no-lonely-if': 'warn',
|
||||
'no-inner-declarations': 'off',
|
||||
'no-unused-vars': 'off',
|
||||
'no-useless-computed-key': 'warn',
|
||||
'no-useless-return': 'warn',
|
||||
'no-var': 'warn',
|
||||
'object-shorthand': ['warn', 'always'],
|
||||
'prefer-arrow-callback': 'warn',
|
||||
'prefer-const': 'warn',
|
||||
'prefer-destructuring': ['warn', { AssignmentExpression: { array: true } }],
|
||||
'prefer-object-spread': 'warn',
|
||||
'prefer-template': 'warn',
|
||||
'spaced-comment': ['warn', 'always', { markers: ['/'] }],
|
||||
yoda: 'warn',
|
||||
'import/order': [
|
||||
'warn',
|
||||
{
|
||||
'newlines-between': 'always',
|
||||
groups: ['type', 'builtin', 'external', 'internal', ['parent', 'sibling'], 'index'],
|
||||
pathGroupsExcludedImportTypes: ['builtin'],
|
||||
pathGroups: [],
|
||||
alphabetize: {
|
||||
order: 'asc',
|
||||
caseInsensitive: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/array-type': ['warn', { default: 'array-simple' }],
|
||||
'@typescript-eslint/ban-ts-comment': [
|
||||
'warn',
|
||||
{
|
||||
'ts-expect-error': 'allow-with-description',
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/consistent-indexed-object-style': ['warn', 'record'],
|
||||
'@typescript-eslint/consistent-type-definitions': ['warn', 'interface'],
|
||||
'@typescript-eslint/no-unused-vars': 'warn',
|
||||
'react/function-component-definition': [
|
||||
'warn',
|
||||
{
|
||||
namedComponents: 'arrow-function',
|
||||
unnamedComponents: 'arrow-function',
|
||||
},
|
||||
],
|
||||
'react/jsx-boolean-value': 'warn',
|
||||
'react/jsx-curly-brace-presence': 'warn',
|
||||
'react/jsx-fragments': 'warn',
|
||||
'react/jsx-no-useless-fragment': ['warn', { allowExpressions: true }],
|
||||
'react/self-closing-comp': 'warn',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['scripts/**/*'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
},
|
||||
},
|
||||
];
|
||||
64
integrations/github/package.json
Normal file
64
integrations/github/package.json
Normal file
@ -0,0 +1,64 @@
|
||||
{
|
||||
"name": "@core/github",
|
||||
"version": "0.1.0",
|
||||
"description": "github extension for CORE",
|
||||
"main": "./bin/index.js",
|
||||
"module": "./bin/index.mjs",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"github",
|
||||
"bin"
|
||||
],
|
||||
"bin": {
|
||||
"github": "./bin/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rimraf bin && npx tsup",
|
||||
"lint": "eslint --ext js,ts,tsx backend/ frontend/ --fix",
|
||||
"prettier": "prettier --config .prettierrc --write .",
|
||||
"copy:spec": "cp spec.json bin/"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@types/node": "^18.0.20",
|
||||
"eslint": "^9.24.0",
|
||||
"eslint-config-prettier": "^10.1.2",
|
||||
"eslint-import-resolver-alias": "^1.1.2",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-jest": "^27.9.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"prettier": "^3.4.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^4.7.2",
|
||||
"tsup": "^8.0.1",
|
||||
"ncc": "0.3.6"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.7.9",
|
||||
"commander": "^12.0.0",
|
||||
"openai": "^4.0.0",
|
||||
"react-query": "^3.39.3",
|
||||
"@redplanethq/sdk": "0.1.2"
|
||||
}
|
||||
}
|
||||
4217
integrations/github/pnpm-lock.yaml
generated
Normal file
4217
integrations/github/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
35
integrations/github/rollup.config.mjs
Normal file
35
integrations/github/rollup.config.mjs
Normal file
@ -0,0 +1,35 @@
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import json from '@rollup/plugin-json';
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import nodePolyfills from 'rollup-plugin-node-polyfills';
|
||||
|
||||
import { terser } from 'rollup-plugin-terser';
|
||||
import typescript from 'rollup-plugin-typescript2';
|
||||
|
||||
export default [
|
||||
{
|
||||
input: 'backend/index.ts',
|
||||
external: ['axios'],
|
||||
output: [
|
||||
{
|
||||
file: 'dist/backend/index.js',
|
||||
sourcemap: true,
|
||||
format: 'cjs',
|
||||
exports: 'named',
|
||||
preserveModules: false,
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
nodePolyfills(),
|
||||
json(),
|
||||
resolve({ extensions: ['.js', '.ts'] }),
|
||||
commonjs({
|
||||
include: /\/node_modules\//,
|
||||
}),
|
||||
typescript({
|
||||
tsconfig: 'tsconfig.json',
|
||||
}),
|
||||
terser(),
|
||||
],
|
||||
},
|
||||
];
|
||||
25
integrations/github/spec.json
Normal file
25
integrations/github/spec.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "GitHub extension",
|
||||
"key": "github",
|
||||
"description": "Plan, track, and manage your agile and software development projects in GitHub. Customize your workflow, collaborate, and release great software.",
|
||||
"icon": "github",
|
||||
"schedule": {
|
||||
"frequency": "*/5 * * * *"
|
||||
},
|
||||
"auth": {
|
||||
"OAuth2": {
|
||||
"token_url": "https://github.com/login/oauth/access_token",
|
||||
"authorization_url": "https://github.com/login/oauth/authorize",
|
||||
"scopes": ["user", "public_repo", "repo", "notifications", "gist", "read:org", "repo_hooks"],
|
||||
"scope_separator": ","
|
||||
}
|
||||
},
|
||||
"mcp": {
|
||||
"type": "http",
|
||||
"url": "https://api.githubcopilot.com/mcp/",
|
||||
"headers": {
|
||||
"Authorization": "Bearer ${config:access_token}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}
|
||||
}
|
||||
37
integrations/github/src/account-create.ts
Normal file
37
integrations/github/src/account-create.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { getGithubData } from './utils';
|
||||
|
||||
export async function integrationCreate(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
data: any,
|
||||
) {
|
||||
const { oauthResponse } = data;
|
||||
const integrationConfiguration = {
|
||||
refresh_token: oauthResponse.refresh_token,
|
||||
access_token: oauthResponse.access_token,
|
||||
};
|
||||
|
||||
const user = await getGithubData(
|
||||
'https://api.github.com/user',
|
||||
integrationConfiguration.access_token,
|
||||
);
|
||||
|
||||
return [
|
||||
{
|
||||
type: 'account',
|
||||
data: {
|
||||
settings: {
|
||||
login: user.login,
|
||||
username: user.login,
|
||||
schedule: {
|
||||
frequency: '*/15 * * * *',
|
||||
},
|
||||
},
|
||||
accountId: user.id.toString(),
|
||||
config: {
|
||||
...integrationConfiguration,
|
||||
mcp: { tokens: { access_token: integrationConfiguration.access_token } },
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
69
integrations/github/src/index.ts
Normal file
69
integrations/github/src/index.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { handleSchedule } from './schedule';
|
||||
import { integrationCreate } from './account-create';
|
||||
|
||||
import {
|
||||
IntegrationCLI,
|
||||
IntegrationEventPayload,
|
||||
IntegrationEventType,
|
||||
Spec,
|
||||
} from '@redplanethq/sdk';
|
||||
|
||||
export async function run(eventPayload: IntegrationEventPayload) {
|
||||
switch (eventPayload.event) {
|
||||
case IntegrationEventType.SETUP:
|
||||
console.log(eventPayload.eventBody);
|
||||
return await integrationCreate(eventPayload.eventBody);
|
||||
|
||||
case IntegrationEventType.SYNC:
|
||||
return await handleSchedule(eventPayload.config, eventPayload.state);
|
||||
|
||||
default:
|
||||
return { message: `The event payload type is ${eventPayload.event}` };
|
||||
}
|
||||
}
|
||||
|
||||
// CLI implementation that extends the base class
|
||||
class GitHubCLI extends IntegrationCLI {
|
||||
constructor() {
|
||||
super('github', '1.0.0');
|
||||
}
|
||||
|
||||
protected async handleEvent(eventPayload: IntegrationEventPayload): Promise<any> {
|
||||
return await run(eventPayload);
|
||||
}
|
||||
|
||||
protected async getSpec(): Promise<Spec> {
|
||||
return {
|
||||
name: 'GitHub extension',
|
||||
key: 'github',
|
||||
description:
|
||||
'Plan, track, and manage your agile and software development projects in GitHub. Customize your workflow, collaborate, and release great software.',
|
||||
icon: 'github',
|
||||
auth: {
|
||||
OAuth2: {
|
||||
token_url: 'https://github.com/login/oauth/access_token',
|
||||
authorization_url: 'https://github.com/login/oauth/authorize',
|
||||
scopes: [
|
||||
'user',
|
||||
'public_repo',
|
||||
'repo',
|
||||
'notifications',
|
||||
'gist',
|
||||
'read:org',
|
||||
'repo_hooks',
|
||||
],
|
||||
scope_separator: ',',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Define a main function and invoke it directly.
|
||||
// This works after bundling to JS and running with `node index.js`.
|
||||
function main() {
|
||||
const githubCLI = new GitHubCLI();
|
||||
githubCLI.parse();
|
||||
}
|
||||
|
||||
main();
|
||||
360
integrations/github/src/schedule.ts
Normal file
360
integrations/github/src/schedule.ts
Normal file
@ -0,0 +1,360 @@
|
||||
import { getUserEvents, getGithubData } from './utils';
|
||||
|
||||
interface GitHubActivityCreateParams {
|
||||
text: string;
|
||||
sourceURL: string;
|
||||
}
|
||||
|
||||
interface GitHubSettings {
|
||||
lastSyncTime?: string;
|
||||
lastUserEventTime?: string;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an activity message based on GitHub data
|
||||
*/
|
||||
function createActivityMessage(params: GitHubActivityCreateParams) {
|
||||
return {
|
||||
type: 'activity',
|
||||
data: {
|
||||
text: params.text,
|
||||
sourceURL: params.sourceURL,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets default sync time (24 hours ago)
|
||||
*/
|
||||
function getDefaultSyncTime(): string {
|
||||
return new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches user information from GitHub
|
||||
*/
|
||||
async function fetchUserInfo(accessToken: string) {
|
||||
try {
|
||||
return await getGithubData('https://api.github.com/user', accessToken);
|
||||
} catch (error) {
|
||||
console.error('Error fetching GitHub user info:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes GitHub notifications into activity messages
|
||||
*/
|
||||
async function processNotifications(accessToken: string, lastSyncTime: string): Promise<any[]> {
|
||||
const activities = [];
|
||||
const allowedReasons = [
|
||||
'assign',
|
||||
'review_requested',
|
||||
'mention',
|
||||
'state_change',
|
||||
'subscribed',
|
||||
'author',
|
||||
'approval_requested',
|
||||
'comment',
|
||||
'ci_activity',
|
||||
'invitation',
|
||||
'member_feature_requested',
|
||||
'security_alert',
|
||||
'security_advisory_credit',
|
||||
'team_mention',
|
||||
];
|
||||
|
||||
let page = 1;
|
||||
let hasMorePages = true;
|
||||
|
||||
while (hasMorePages) {
|
||||
try {
|
||||
const notifications = await getGithubData(
|
||||
`https://api.github.com/notifications?page=${page}&per_page=50&all=true&since=${lastSyncTime}`,
|
||||
accessToken,
|
||||
);
|
||||
|
||||
if (!notifications || notifications.length === 0) {
|
||||
hasMorePages = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (notifications.length < 50) {
|
||||
hasMorePages = false;
|
||||
} else {
|
||||
page++;
|
||||
}
|
||||
|
||||
for (const notification of notifications) {
|
||||
try {
|
||||
if (!allowedReasons.includes(notification.reason)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const repository = notification.repository;
|
||||
const subject = notification.subject;
|
||||
let title = '';
|
||||
let sourceURL = '';
|
||||
|
||||
// Get the actual GitHub data for the notification
|
||||
let githubData: any = {};
|
||||
if (subject.url) {
|
||||
try {
|
||||
githubData = await getGithubData(subject.url, accessToken);
|
||||
} catch (error) {
|
||||
console.error('Error fetching GitHub data for notification:', error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const url = githubData.html_url || notification.subject.url || '';
|
||||
sourceURL = url;
|
||||
|
||||
const isIssue = subject.type === 'Issue';
|
||||
const isPullRequest = subject.type === 'PullRequest';
|
||||
const isComment = notification.reason === 'comment';
|
||||
|
||||
switch (notification.reason) {
|
||||
case 'assign':
|
||||
title = `${isIssue ? 'Issue' : 'PR'} assigned to you: #${githubData.number} - ${githubData.title}`;
|
||||
break;
|
||||
|
||||
case 'author':
|
||||
if (isComment) {
|
||||
title = `New comment on your ${isIssue ? 'issue' : 'PR'} by ${githubData.user?.login}: ${githubData.body}`;
|
||||
} else {
|
||||
title = `You created this ${isIssue ? 'issue' : 'PR'}: #${githubData.number} - ${githubData.title}`;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'comment':
|
||||
title = `New comment by ${githubData.user?.login} in ${repository.full_name}: ${githubData.body}`;
|
||||
break;
|
||||
|
||||
case 'manual':
|
||||
title = `You subscribed to: #${githubData.number} - ${githubData.title}`;
|
||||
break;
|
||||
|
||||
case 'mention':
|
||||
title = `@mentioned by ${githubData.user?.login} in ${repository.full_name}: ${githubData.body}`;
|
||||
break;
|
||||
|
||||
case 'review_requested':
|
||||
title = `PR review requested in ${repository.full_name}: #${githubData.number} - ${githubData.title}`;
|
||||
break;
|
||||
|
||||
case 'state_change': {
|
||||
let stateInfo = '';
|
||||
if (githubData.state) {
|
||||
stateInfo = `to ${githubData.state}`;
|
||||
} else if (githubData.merged) {
|
||||
stateInfo = 'to merged';
|
||||
} else if (githubData.closed_at) {
|
||||
stateInfo = 'to closed';
|
||||
}
|
||||
title = `State changed ${stateInfo} in ${repository.full_name}: #${githubData.number} - ${githubData.title}`;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'subscribed':
|
||||
if (isComment) {
|
||||
title = `New comment on watched ${isIssue ? 'issue' : 'PR'} in ${repository.full_name} by ${githubData.user?.login}: ${githubData.body}`;
|
||||
} else if (isPullRequest) {
|
||||
title = `New PR created in watched repo ${repository.full_name}: #${githubData.number} - ${githubData.title}`;
|
||||
} else if (isIssue) {
|
||||
title = `New issue created in watched repo ${repository.full_name}: #${githubData.number} - ${githubData.title}`;
|
||||
} else {
|
||||
title = `Update in watched repo ${repository.full_name}: #${githubData.number} - ${githubData.title}`;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'team_mention':
|
||||
title = `Your team was mentioned in ${repository.full_name}`;
|
||||
break;
|
||||
|
||||
default:
|
||||
title = `GitHub notification: ${repository.full_name}`;
|
||||
break;
|
||||
}
|
||||
|
||||
if (title && sourceURL) {
|
||||
activities.push(
|
||||
createActivityMessage({
|
||||
text: title,
|
||||
sourceURL: sourceURL,
|
||||
}),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
// Silently ignore errors to prevent stdout pollution
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Silently ignore errors to prevent stdout pollution
|
||||
hasMorePages = false;
|
||||
}
|
||||
}
|
||||
|
||||
return activities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes user events (PRs, issues, comments) into activity messages
|
||||
*/
|
||||
async function processUserEvents(
|
||||
username: string,
|
||||
accessToken: string,
|
||||
lastUserEventTime: string,
|
||||
): Promise<any[]> {
|
||||
const activities = [];
|
||||
let page = 1;
|
||||
let hasMorePages = true;
|
||||
|
||||
console.log('Processing user events');
|
||||
|
||||
while (hasMorePages) {
|
||||
try {
|
||||
const userEvents = await getUserEvents(username, page, accessToken, lastUserEventTime);
|
||||
console.log('User events', userEvents);
|
||||
|
||||
if (!userEvents || userEvents.length === 0) {
|
||||
hasMorePages = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (userEvents.length < 30) {
|
||||
hasMorePages = false;
|
||||
} else {
|
||||
page++;
|
||||
}
|
||||
|
||||
for (const event of userEvents) {
|
||||
try {
|
||||
let title = '';
|
||||
const sourceURL = event.html_url || '';
|
||||
|
||||
switch (event.type) {
|
||||
case 'pr':
|
||||
title = `You created PR #${event.number}: ${event.title}`;
|
||||
break;
|
||||
case 'issue':
|
||||
title = `You created issue #${event.number}: ${event.title}`;
|
||||
break;
|
||||
case 'pr_comment':
|
||||
title = `You commented on PR #${event.number}: ${event.title}`;
|
||||
break;
|
||||
case 'issue_comment':
|
||||
title = `You commented on issue #${event.number}: ${event.title}`;
|
||||
break;
|
||||
case 'self_assigned_issue':
|
||||
title = `You assigned yourself to issue #${event.number}: ${event.title}`;
|
||||
break;
|
||||
default:
|
||||
title = `GitHub activity: ${event.title || 'Unknown'}`;
|
||||
break;
|
||||
}
|
||||
|
||||
if (title && sourceURL) {
|
||||
activities.push(
|
||||
createActivityMessage({
|
||||
text: title,
|
||||
sourceURL: sourceURL,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
console.log('Activities', activities);
|
||||
} catch (error) {
|
||||
// Silently ignore errors to prevent stdout pollution
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Silently ignore errors to prevent stdout pollution
|
||||
hasMorePages = false;
|
||||
}
|
||||
}
|
||||
|
||||
return activities;
|
||||
}
|
||||
|
||||
export async function handleSchedule(config: any, state: any) {
|
||||
try {
|
||||
const integrationConfiguration = config;
|
||||
|
||||
// Check if we have a valid access token
|
||||
if (!integrationConfiguration?.access_token) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get settings or initialize if not present
|
||||
let settings = (state || {}) as GitHubSettings;
|
||||
|
||||
// Default to 24 hours ago if no last sync times
|
||||
const lastSyncTime = settings.lastSyncTime || getDefaultSyncTime();
|
||||
const lastUserEventTime = settings.lastUserEventTime || getDefaultSyncTime();
|
||||
|
||||
// Fetch user info to get username if not available
|
||||
let user;
|
||||
try {
|
||||
user = await fetchUserInfo(integrationConfiguration.access_token);
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Update username in settings if not present
|
||||
if (!settings.username && user.login) {
|
||||
settings.username = user.login;
|
||||
}
|
||||
|
||||
// Collect all messages
|
||||
const messages = [];
|
||||
|
||||
// Process notifications
|
||||
try {
|
||||
const notificationActivities = await processNotifications(
|
||||
integrationConfiguration.access_token,
|
||||
lastSyncTime,
|
||||
);
|
||||
messages.push(...notificationActivities);
|
||||
} catch (error) {
|
||||
// Silently ignore errors to prevent stdout pollution
|
||||
}
|
||||
|
||||
// Process user events if we have a username
|
||||
if (settings.username) {
|
||||
console.log('Processing user events');
|
||||
try {
|
||||
const userEventActivities = await processUserEvents(
|
||||
settings.username,
|
||||
integrationConfiguration.access_token,
|
||||
lastUserEventTime,
|
||||
);
|
||||
messages.push(...userEventActivities);
|
||||
} catch (error) {
|
||||
// Silently ignore errors to prevent stdout pollution
|
||||
}
|
||||
}
|
||||
|
||||
// Update last sync times
|
||||
const newSyncTime = new Date().toISOString();
|
||||
|
||||
// Add state message for saving settings
|
||||
messages.push({
|
||||
type: 'state',
|
||||
data: {
|
||||
...settings,
|
||||
lastSyncTime: newSyncTime,
|
||||
lastUserEventTime: newSyncTime,
|
||||
},
|
||||
});
|
||||
|
||||
return messages;
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
97
integrations/github/src/utils.ts
Normal file
97
integrations/github/src/utils.ts
Normal file
@ -0,0 +1,97 @@
|
||||
import axios from 'axios';
|
||||
|
||||
export async function getGithubData(url: string, accessToken: string) {
|
||||
return (
|
||||
await axios.get(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: 'application/vnd.github+json',
|
||||
'X-GitHub-Api-Version': '2022-11-28',
|
||||
},
|
||||
})
|
||||
).data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user events (PRs, issues, comments) and also issues assigned to the user by themselves.
|
||||
*/
|
||||
export async function getUserEvents(
|
||||
username: string,
|
||||
page: number,
|
||||
accessToken: string,
|
||||
since?: string,
|
||||
) {
|
||||
try {
|
||||
const formattedDate = since ? encodeURIComponent(since.split('T')[0]) : '';
|
||||
// Search for user's PRs, issues, and comments since the last sync
|
||||
const [
|
||||
prsResponse,
|
||||
issuesResponse,
|
||||
commentsResponse,
|
||||
// For self-assigned issues, we need to fetch issues assigned to the user and authored by the user
|
||||
assignedIssuesResponse,
|
||||
] = await Promise.all([
|
||||
// Search for PRs created by user
|
||||
getGithubData(
|
||||
`https://api.github.com/search/issues?q=author:${username}+type:pr+created:>${formattedDate}&sort=created&order=desc&page=${page}&per_page=10`,
|
||||
accessToken,
|
||||
),
|
||||
// Search for issues created by user
|
||||
getGithubData(
|
||||
`https://api.github.com/search/issues?q=author:${username}+type:issue+created:>${formattedDate}&sort=created&order=desc&page=${page}&per_page=10`,
|
||||
accessToken,
|
||||
),
|
||||
// Search for issues/PRs the user commented on
|
||||
getGithubData(
|
||||
`https://api.github.com/search/issues?q=commenter:${username}+updated:>${formattedDate}&sort=updated&order=desc&page=${page}&per_page=10`,
|
||||
accessToken,
|
||||
),
|
||||
// Search for issues assigned to the user and authored by the user (self-assigned)
|
||||
getGithubData(
|
||||
`https://api.github.com/search/issues?q=assignee:${username}+author:${username}+type:issue+updated:>${formattedDate}&sort=updated&order=desc&page=${page}&per_page=10`,
|
||||
accessToken,
|
||||
),
|
||||
]);
|
||||
|
||||
console.log('PRs found:', prsResponse?.items?.length || 0);
|
||||
console.log('Issues found:', issuesResponse?.items?.length || 0);
|
||||
console.log('Comments found:', commentsResponse?.items?.length || 0);
|
||||
console.log('Self-assigned issues found:', assignedIssuesResponse?.items?.length || 0);
|
||||
|
||||
// Return simplified results - combine PRs, issues, commented items, and self-assigned issues
|
||||
const results = [
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
...(prsResponse?.items || []).map((item: any) => ({ ...item, type: 'pr' })),
|
||||
...(issuesResponse?.items || [])
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
.filter((item: any) => !item.pull_request)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
.map((item: any) => ({ ...item, type: 'issue' })),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
...(commentsResponse?.items || []).map((item: any) => ({
|
||||
...item,
|
||||
type: item.pull_request ? 'pr_comment' : 'issue_comment',
|
||||
})),
|
||||
// Add self-assigned issues, but only if not already present in issuesResponse
|
||||
...(assignedIssuesResponse?.items || [])
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
.filter((item: any) => {
|
||||
// Only include if not already in issuesResponse (by id)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return !(issuesResponse?.items || []).some((issue: any) => issue.id === item.id);
|
||||
})
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
.map((item: any) => ({
|
||||
...item,
|
||||
type: 'self_assigned_issue',
|
||||
})),
|
||||
];
|
||||
|
||||
// Sort by created_at descending
|
||||
results.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
||||
return results;
|
||||
} catch (error) {
|
||||
console.error('Error fetching user activity via search:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
32
integrations/github/tsconfig.json
Normal file
32
integrations/github/tsconfig.json
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2022",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"baseUrl": "frontend",
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"strictNullChecks": true,
|
||||
"removeComments": true,
|
||||
"preserveConstEnums": true,
|
||||
"sourceMap": true,
|
||||
"noUnusedParameters": true,
|
||||
"noUnusedLocals": true,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitThis": true,
|
||||
"noImplicitAny": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"useUnknownInCatchVariables": false
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "build", "dist", "scripts", "acceptance-tests", "webpack", "jest"],
|
||||
"types": ["typePatches"]
|
||||
}
|
||||
20
integrations/github/tsup.config.ts
Normal file
20
integrations/github/tsup.config.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
import { dependencies } from './package.json';
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/index.ts'],
|
||||
format: ['cjs'], // or esm if you're using that
|
||||
bundle: true,
|
||||
target: 'node16',
|
||||
outDir: 'bin',
|
||||
splitting: false,
|
||||
shims: true,
|
||||
clean: true,
|
||||
name: 'github',
|
||||
platform: 'node',
|
||||
legacyOutput: false,
|
||||
noExternal: Object.keys(dependencies || {}), // ⬅️ bundle all deps
|
||||
treeshake: {
|
||||
preset: 'recommended',
|
||||
},
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user