mirror of
https://github.com/eliasstepanik/core.git
synced 2026-01-20 22:08: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 { logger } from "~/services/logger.service";
|
||||||
import { prisma } from "~/db.server";
|
import { prisma } from "~/db.server";
|
||||||
import { triggerIntegrationWebhook } from "~/trigger/webhooks/integration-webhook-delivery";
|
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) {
|
export async function action({ request }: ActionFunctionArgs) {
|
||||||
if (request.method !== "POST") {
|
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(
|
await triggerIntegrationWebhook(
|
||||||
integrationAccountId,
|
integrationAccountId,
|
||||||
userId,
|
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